musicians
<%= render "screen_navigation" %>This feature not yet implemented
+ + +diff --git a/admin/Gemfile b/admin/Gemfile index 7d49040dd..f80ab1d01 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -41,6 +41,7 @@ gem 'rails3-jquery-autocomplete' gem 'activeadmin' gem "meta_search", '>= 1.1.0.pre' gem 'fog', "~> 1.3.1" +gem 'unf' #optional fog dependency gem 'country-select' gem 'aasm', '3.0.16' gem 'postgres-copy' @@ -56,6 +57,9 @@ gem 'ruby-protocol-buffers', '1.2.2' gem 'sendgrid', '1.1.0' +gem 'geokit-rails' +gem 'postgres_ext' + group :libv8 do gem 'libv8', "~> 3.11.8" end diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index 438107c40..227ce212f 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -9,7 +9,7 @@ FactoryGirl.define do musician true city "Apex" state "NC" - country "USA" + country "US" terms_of_service true diff --git a/db/manifest b/db/manifest index 13ef8931c..94d24481b 100755 --- a/db/manifest +++ b/db/manifest @@ -74,4 +74,5 @@ crash_dumps_idx.sql music_sessions_user_history_add_session_removed_at.sql user_progress_tracking.sql whats_next.sql -add_user_bio.sql \ No newline at end of file +add_user_bio.sql +users_geocoding.sql diff --git a/db/up/update_user_band_fields.sql b/db/up/update_user_band_fields.sql index a7899817f..06ebcfe45 100644 --- a/db/up/update_user_band_fields.sql +++ b/db/up/update_user_band_fields.sql @@ -7,7 +7,7 @@ alter table users alter column birth_date drop not null; update users set state = 'NC'; alter table users alter column state set not null; -update users set country = 'USA'; +update users set country = 'US'; alter table users alter column country set not null; alter table bands alter column city drop default; @@ -15,7 +15,7 @@ alter table bands alter column city drop default; update users set state = 'NC'; alter table bands alter column state set not null; -update users set country = 'USA'; +update users set country = 'US'; alter table bands alter column country set not null; --alter table users drop column account_id; diff --git a/db/up/users_geocoding.sql b/db/up/users_geocoding.sql new file mode 100644 index 000000000..667f86824 --- /dev/null +++ b/db/up/users_geocoding.sql @@ -0,0 +1,11 @@ +ALTER TABLE users ADD COLUMN lat NUMERIC(15,10); +ALTER TABLE users ADD COLUMN lng NUMERIC(15,10); + +ALTER TABLE max_mind_geo ADD COLUMN lat NUMERIC(15,10); +ALTER TABLE max_mind_geo ADD COLUMN lng NUMERIC(15,10); +ALTER TABLE max_mind_geo DROP COLUMN ip_bottom; +ALTER TABLE max_mind_geo DROP COLUMN ip_top; +ALTER TABLE max_mind_geo ADD COLUMN ip_start INET; +ALTER TABLE max_mind_geo ADD COLUMN ip_end INET; + +UPDATE users SET country = 'US' WHERE country = 'USA'; diff --git a/ruby/Gemfile b/ruby/Gemfile index 86c0ca399..907ca1b79 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -24,6 +24,9 @@ gem 'aasm', '3.0.16' gem 'devise', '>= 1.1.2' gem 'postgres-copy' +gem 'geokit-rails' +gem 'postgres_ext' + if devenv gem 'jam_db', :path=> "../db/target/ruby_package" gem 'jampb', :path => "../pb/target/ruby/jampb" diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index baea30286..763a6ccbc 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -11,6 +11,9 @@ require "action_mailer" require "devise" require "sendgrid" require "postgres-copy" +require "geokit-rails" +require "postgres_ext" + require "jam_ruby/lib/module_overrides" require "jam_ruby/constants/limits" require "jam_ruby/constants/notification_types" @@ -82,4 +85,4 @@ include Jampb module JamRuby -end \ No newline at end of file +end diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index d66cfbd5a..62935f4e3 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -21,6 +21,7 @@ module JamRuby validates :as_musician, :inclusion => {:in => [true, false]} validate :can_join_music_session, :if => :joining_session? after_save :require_at_least_one_track_when_in_session, :if => :joining_session? + after_create :did_create include AASM IDLE_STATE = :idle @@ -112,6 +113,10 @@ module JamRuby return self.music_session.users.exists?(user) end + def did_create + self.user.update_lat_lng(self.ip_address) if self.user && self.ip_address + end + private def require_at_least_one_track_when_in_session if tracks.count == 0 diff --git a/ruby/lib/jam_ruby/models/max_mind_geo.rb b/ruby/lib/jam_ruby/models/max_mind_geo.rb index 0c8f2c082..81868c4cd 100644 --- a/ruby/lib/jam_ruby/models/max_mind_geo.rb +++ b/ruby/lib/jam_ruby/models/max_mind_geo.rb @@ -3,36 +3,44 @@ module JamRuby self.table_name = 'max_mind_geo' + def self.ip_lookup(ip_addy) + self.where(["ip_start <= ? AND ip_end >= ?", + ip_addy, ip_addy]) + .limit(1) + .first + end def self.import_from_max_mind(file) - # File Geo-124 + # File Geo-139 # Format: # startIpNum,endIpNum,country,region,city,postalCode,latitude,longitude,dmaCode,areaCode MaxMindGeo.transaction do + cols = [:startIpNum, :endIpNum, :country, :region, :city, :latitude, :longitude] MaxMindGeo.delete_all File.open(file, 'r:ISO-8859-1') do |io| - MaxMindGeo.pg_copy_from io, :map => { 'startIpNum' => 'ip_bottom', 'endIpNum' => 'ip_top', 'country' => 'country', 'region' => 'region', 'city' => 'city'}, :columns => [:startIpNum, :endIpNum, :country, :region, :city] do |row| - row[0] = ip_address_to_int(row[0]) - row[1] = ip_address_to_int(row[1]) - row.delete_at(5) - row.delete_at(5) - row.delete_at(5) - row.delete_at(5) + MaxMindGeo.pg_copy_from(io, :map => { + 'startIpNum' => 'ip_start', + 'endIpNum' => 'ip_end', + 'country' => 'country', + 'region' => 'region', + 'city' => 'city', + 'latitude' => 'lat', + 'longitude' => 'lng'}, + :columns => cols) do |row| + row[0] = row[0] + row[1] = row[1] + row[2] = MaxMindIsp.strip_quotes(row[2]) + row[3] = MaxMindIsp.strip_quotes(row[3]) + row[4] = MaxMindIsp.strip_quotes(row[4]) row.delete_at(5) + row.delete_at(-1) if 8 <= row.count + row.delete_at(-1) if 8 <= row.count end end end - end - - - # Make an IP address fit in a signed int. Just divide it by 2, as the least significant part - # just can't possibly matter. We can verify this if needed. My guess is the entire bottom octet is - # actually irrelevant - def self.ip_address_to_int(ip) - ip.split('.').inject(0) {|total,value| (total << 8 ) + value.to_i} / 2 + User.find_each { |usr| usr.update_lat_lng } end end - -end \ No newline at end of file +end diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index c3c33c7e2..377b9fb32 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -5,6 +5,17 @@ module JamRuby LIMIT = 10 + ORDER_FOLLOWS = ['Most Followed', :followed] + ORDER_PLAYS = ['Most Plays', :plays] + ORDER_PLAYING = ['Playing Now', :playing] + ORDERINGS = [ORDER_FOLLOWS, ORDER_PLAYS, ORDER_PLAYING] + ORDERING_KEYS = ORDERINGS.collect { |oo| oo[1] } + + def self.order_param(params) + ordering = params[:orderby] + ordering.blank? ? ORDERING_KEYS[0] : ordering + end + # performs a site-white search def self.search(query, user_id = nil) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 037c490f2..b2c07ff65 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -8,8 +8,12 @@ module JamRuby devise :database_authenticatable, :recoverable, :rememberable + include Geokit::ActsAsMappable::Glue unless defined?(acts_as_mappable) + acts_as_mappable - attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_s3_path, :photo_url, :crop_selection + after_save :check_lat_lng + + attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_s3_path, :photo_url, :crop_selection, :lat, :lng # updating_password corresponds to a lost_password attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field @@ -900,6 +904,50 @@ module JamRuby end end + def self.musician_search(params={}, current_user=nil) + rel = User.where(:musician => true) + unless (instrument = params[:instrument]).blank? + rel = rel.joins("RIGHT JOIN musicians_instruments AS minst ON minst.user_id = users.id") + .where(['minst.instrument_id = ? AND users.id IS NOT NULL', instrument]) + end + + location_distance, location_city = params[:distance], params[:city] + if location_distance && location_city + if geo = MaxMindGeo.where(:city => params[:city]).limit(1).first + citylatlng = [geo.lat, geo.lng] + rel = rel.within(location_distance, :origin => citylatlng) + end + elsif current_user + latlng = [] + if current_user.lat.nil? + if params[:remote_ip] + if geo = MaxMindGeo.ip_lookup(params[:remote_ip]) + latlng = [geo.lat, geo.lng] + end + end + else + latlng = [current_user.lat, current_user.lng] + end + distance = location_distance || 50 + rel = rel.within(distance, :origin => latlng) unless latlng.blank? + end + + case ordering = Search.order_param(params) + when :plays + when :followed + rel = rel.select("COUNT(follows) AS fcount, users.id") + rel = rel.joins("LEFT JOIN users_followers AS follows ON follows.user_id = users.id") + rel = rel.group("users.id") + rel = rel.order("COUNT(follows) DESC") + when :playing + end + perpage = params[:per_page] || 20 + page = [params[:page].to_i, 1].max + rel = rel.paginate(:page => page, :per_page => perpage) + # puts rel.to_sql + rel + end + def self.search(query, options = { :limit => 10 }) # only issue search if at least 2 characters are specified @@ -925,6 +973,44 @@ module JamRuby .limit(options[:limit]) end + def provides_location? + !self.city.blank? && (!self.state.blank? || !self.country.blank?) + end + + def check_lat_lng + if (city_changed? || state_changed? || country_changed?) && !lat_changed? && !lng_changed? + update_lat_lng + end + end + + def update_lat_lng(ip_addy=nil) + if provides_location? # ip_addy argument ignored in this case + return false unless ip_addy.nil? # do nothing if attempting to set latlng from an ip address + query = { :city => self.city } + query[:region] = self.state unless self.state.blank? + query[:country] = self.country unless self.country.blank? + if geo = MaxMindGeo.where(query).limit(1).first + if geo.lat && geo.lng && (self.lat != geo.lat || self.lng != geo.lng) + self.update_attributes({ :lat => geo.lat, :lng => geo.lng }) + return true + end + end + elsif ip_addy + if geo = MaxMindGeo.ip_lookup(ip_addy) + if self.lat != geo.lat || self.lng != geo.lng + self.update_attributes({ :lat => geo.lat, :lng => geo.lng }) + return true + end + end + else + if self.lat || self.lng + self.update_attributes({ :lat => nil, :lng => nil }) + return true + end + end + false + end + # devise compatibility #def encrypted_password diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 951cd7232..5de772b2b 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -8,7 +8,7 @@ FactoryGirl.define do email_confirmed true city "Apex" state "NC" - country "USA" + country "US" musician true terms_of_service true @@ -77,7 +77,7 @@ FactoryGirl.define do biography "My Biography" city "Apex" state "NC" - country "USA" + country "US" end factory :genre, :class => JamRuby::Genre do @@ -121,4 +121,15 @@ FactoryGirl.define do factory :crash_dump, :class => JamRuby::CrashDump do end + + factory :geocoder, :class => JamRuby::MaxMindGeo do + country 'US' + sequence(:region) { |n| ['NC', 'CA'][(n-1).modulo(2)] } + sequence(:city) { |n| ['Apex', 'San Francisco'][(n-1).modulo(2)] } + sequence(:ip_start) { |n| ['1.1.0.0', '1.1.255.255'][(n-1).modulo(2)] } + sequence(:ip_end) { |n| ['1.2.0.0', '1.2.255.255'][(n-1).modulo(2)] } + sequence(:lat) { |n| [35.73265, 37.7742075][(n-1).modulo(2)] } + sequence(:lng) { |n| [-78.85029, -122.4155311][(n-1).modulo(2)] } + end + end diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index 0056e9486..aacd9f6c9 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -12,7 +12,7 @@ describe ConnectionManager do end def create_user(first_name, last_name, email, options = {:musician => true}) - @conn.exec("INSERT INTO users (first_name, last_name, email, musician, encrypted_password, city, state, country) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id", [first_name, last_name, email, options[:musician], '1', 'Apex', 'NC', 'USA']) do |result| + @conn.exec("INSERT INTO users (first_name, last_name, email, musician, encrypted_password, city, state, country) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id", [first_name, last_name, email, options[:musician], '1', 'Apex', 'NC', 'US']) do |result| return result.getvalue(0, 0) end end diff --git a/ruby/spec/jam_ruby/models/band_search_spec.rb b/ruby/spec/jam_ruby/models/band_search_spec.rb index 99af288f8..2af217b3f 100644 --- a/ruby/spec/jam_ruby/models/band_search_spec.rb +++ b/ruby/spec/jam_ruby/models/band_search_spec.rb @@ -6,7 +6,7 @@ describe User do before(:each) do - @band = Band.save(nil, "Example Band", "www.bands.com", "zomg we rock", "Apex", "NC", "USA", ["hip hop"], user.id, nil, nil) + @band = Band.save(nil, "Example Band", "www.bands.com", "zomg we rock", "Apex", "NC", "US", ["hip hop"], user.id, nil, nil) end @@ -93,7 +93,7 @@ describe User do end it "should tokenize correctly" do - @band2 = Band.save(nil, "Peach pit", "www.bands.com", "zomg we rock", "Apex", "NC", "USA", ["hip hop"], user.id, nil, nil) + @band2 = Band.save(nil, "Peach pit", "www.bands.com", "zomg we rock", "Apex", "NC", "US", ["hip hop"], user.id, nil, nil) ws = Band.search("pea") ws.length.should == 1 user_result = ws[0] @@ -102,7 +102,7 @@ describe User do it "should not return anything with a 1 character search" do - @band2 = Band.save(nil, "Peach pit", "www.bands.com", "zomg we rock", "Apex", "NC", "USA", ["hip hop"], user.id, nil, nil) + @band2 = Band.save(nil, "Peach pit", "www.bands.com", "zomg we rock", "Apex", "NC", "US", ["hip hop"], user.id, nil, nil) ws = Band.search("pe") ws.length.should == 1 user_result = ws[0] @@ -111,4 +111,4 @@ describe User do ws = Band.search("p") ws.length.should == 0 end -end \ No newline at end of file +end diff --git a/ruby/spec/jam_ruby/models/connection_spec.rb b/ruby/spec/jam_ruby/models/connection_spec.rb index 29d23f476..e40db1611 100644 --- a/ruby/spec/jam_ruby/models/connection_spec.rb +++ b/ruby/spec/jam_ruby/models/connection_spec.rb @@ -33,4 +33,18 @@ describe Connection do connection.destroyed?.should be_true end + it 'updates user lat/lng' do + uu = FactoryGirl.create(:user) + uu.lat.should == nil + msess = FactoryGirl.create(:music_session, :creator => uu) + geocode = FactoryGirl.create(:geocoder) + connection = FactoryGirl.create(:connection, + :user => uu, + :music_session => msess, + :ip_address => "1.1.1.1", + :client_id => "1") + user.lat.should == geocode.lat + user.lng.should == geocode.lng + end + end diff --git a/ruby/spec/jam_ruby/models/max_mind_geo_spec.rb b/ruby/spec/jam_ruby/models/max_mind_geo_spec.rb index ba49462de..1df42817d 100644 --- a/ruby/spec/jam_ruby/models/max_mind_geo_spec.rb +++ b/ruby/spec/jam_ruby/models/max_mind_geo_spec.rb @@ -9,31 +9,30 @@ describe MaxMindGeo do in_directory_with_file(GEO_CSV) before do - content_for_file('startIpNum,endIpNum,country,region,city,postalCode,latitude,longitude,dmaCode,areaCode -0.116.0.0,0.119.255.255,"AT","","","",47.3333,13.3333,, +0.116.0.0,0.119.255.255,"AT","","","",47.3333,13.3333,123,123 1.0.0.0,1.0.0.255,"AU","","","",-27.0000,133.0000,, 1.0.1.0,1.0.1.255,"CN","07","Fuzhou","",26.0614,119.3061,,'.encode(Encoding::ISO_8859_1)) MaxMindGeo.import_from_max_mind(GEO_CSV) end - let(:first) { MaxMindGeo.find_by_ip_bottom(MaxMindGeo.ip_address_to_int('0.116.0.0')) } - let(:second) { MaxMindGeo.find_by_ip_bottom(MaxMindGeo.ip_address_to_int('1.0.0.0')) } - let(:third) { MaxMindGeo.find_by_ip_bottom(MaxMindGeo.ip_address_to_int('1.0.1.0')) } - it { MaxMindGeo.count.should == 3 } + let(:first) { MaxMindGeo.find_by_ip_start('0.116.0.0') } + let(:second) { MaxMindGeo.find_by_ip_start('1.0.0.0') } + let(:third) { MaxMindGeo.find_by_ip_start('1.0.1.0') } + it { first.country.should == 'AT' } - it { first.ip_bottom.should == MaxMindGeo.ip_address_to_int('0.116.0.0') } - it { first.ip_top.should == MaxMindGeo.ip_address_to_int('0.119.255.255') } + it { first.ip_start.should == '0.116.0.0' } + it { first.ip_end.should == '0.119.255.255' } it { second.country.should == 'AU' } - it { second.ip_bottom.should == MaxMindGeo.ip_address_to_int('1.0.0.0') } - it { second.ip_top.should == MaxMindGeo.ip_address_to_int('1.0.0.255') } + it { second.ip_start.should == '1.0.0.0' } + it { second.ip_end.should == '1.0.0.255' } it { third.country.should == 'CN' } - it { third.ip_bottom.should == MaxMindGeo.ip_address_to_int('1.0.1.0') } - it { third.ip_top.should == MaxMindGeo.ip_address_to_int('1.0.1.255') } + it { third.ip_start.should == '1.0.1.0' } + it { third.ip_end.should == '1.0.1.255' } end diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb new file mode 100644 index 000000000..a8aec246a --- /dev/null +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +describe User do + + before(:each) do + @geocode1 = FactoryGirl.create(:geocoder) + @geocode2 = FactoryGirl.create(:geocoder) + params = { + first_name: "Example", + last_name: "User", + email: "user1@example.com", + password: "foobar", + password_confirmation: "foobar", + musician: true, + email_confirmed: true, + city: "Apex", + state: "NC", + country: "US" + } + @users = [] + @users << @user1 = FactoryGirl.create(:user, params) + params[:email] = "user2@example.com" + @users << @user2 = FactoryGirl.create(:user, params) + params[:email] = "user3@example.com" + @users << @user3 = FactoryGirl.create(:user, params) + params[:email] = "user4@example.com" + @users << @user4 = FactoryGirl.create(:user, params) + end + + it "should find all musicians sorted by followers with pagination" do + # establish sorting order + @user4.followers.concat([@user2, @user3, @user4]) + @user3.followers.concat([@user3, @user4]) + @user2.followers.concat([@user1]) + @user4.followers.count.should == 3 + + UserFollower.count.should == 6 + + # get all the users in correct order + params = { :per_page => @users.size } + results = User.musician_search(params) + results.all.count.should == @users.size + + results.each_with_index do |uu, idx| + uu.id.should == @users.reverse[idx].id + end + + # refresh the order to ensure it works right + @user2.followers.concat([@user3, @user4, @user2]) + results = User.musician_search(params) + results[0].id.should == @user2.id + + # make sure pagination works right + params = { :per_page => 2, :page => 1 } + results = User.musician_search(params) + results.all.count.should == 2 + end + + it "should find all musicians sorted by plays " do + pending + end + + it "should find all musicians sorted by now playing" do + pending + end + + it "should find musicians with 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' } + ii.should_not be_nil + params = { :instrument => ii.id } + results = User.musician_search(params) + results.all.each do |rr| + rr.instruments.detect { |inst| inst.id=='tuba' }.id.should == ii.id + end + results.all.count.should == 1 + end + + it "should find musicians within a given distance of location" do + @user1.lat.should_not == nil + params = { :distance => 10, :city => 'San Francisco' } + results = User.musician_search(params) + results.all.count.should == 0 + + params = { :distance => 10, :city => 'Apex' } + results = User.musician_search(params) + results.all.count.should == User.count + + params = { :distance => 10 } + results = User.musician_search(params) + results.all.count.should == User.count + end + +end diff --git a/ruby/spec/jam_ruby/models/search_spec.rb b/ruby/spec/jam_ruby/models/search_spec.rb index b0798c570..65b658269 100644 --- a/ruby/spec/jam_ruby/models/search_spec.rb +++ b/ruby/spec/jam_ruby/models/search_spec.rb @@ -7,9 +7,9 @@ describe Search do def create_peachy_data - @user = FactoryGirl.create(:user, first_name: "Peach", last_name: "Pit", email: "user@example.com", musician: true, city: "Apex", state: "NC", country:"USA") - @fan = FactoryGirl.create(:user, first_name: "Peach Peach", last_name: "Pit", email: "fan@example.com", musician: false, city: "Apex", state: "NC", country:"USA") - @band = FactoryGirl.create(:band, name: "Peach pit", website: "www.bands.com", biography: "zomg we rock", city: "Apex", state: "NC", country:"USA") + @user = FactoryGirl.create(:user, first_name: "Peach", last_name: "Pit", email: "user@example.com", musician: true, city: "Apex", state: "NC", country:"US") + @fan = FactoryGirl.create(:user, first_name: "Peach Peach", last_name: "Pit", email: "fan@example.com", musician: false, city: "Apex", state: "NC", country:"US") + @band = FactoryGirl.create(:band, name: "Peach pit", website: "www.bands.com", biography: "zomg we rock", city: "Apex", state: "NC", country:"US") end def assert_peachy_data diff --git a/ruby/spec/jam_ruby/models/user_location_spec.rb b/ruby/spec/jam_ruby/models/user_location_spec.rb new file mode 100644 index 000000000..3a7d68020 --- /dev/null +++ b/ruby/spec/jam_ruby/models/user_location_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe User do + +=begin +X If user provides profile location data, that will be used for lat/lng lookup +X If the user changes their profile location, we update their lat/lng address +X If no profile location is provided, then we populate lat/lng from their IP address +X If no profile location is provided, and the user creates/joins a music session, then we update their lat/lng from the IP address +=end + + before do + @geocode1 = FactoryGirl.create(:geocoder) + @geocode2 = FactoryGirl.create(:geocoder) + @user = User.new(first_name: "Example", last_name: "User", email: "user@example.com", + password: "foobar", password_confirmation: "foobar", + city: "Apex", state: "NC", country: "US", + terms_of_service: true, musician: true) + @user.save! + end + + describe "with profile location data" do + it "should have lat/lng values" do + geo = MaxMindGeo.find_by_city(@user.city) + @user.lat.should == geo.lat + @user.lng.should == geo.lng + end + it "should have updated lat/lng values" do + @user.update_attributes({ :city => @geocode2.city, + :state => @geocode2.region, + :country => @geocode2.country, + }) + geo = MaxMindGeo.find_by_city(@user.city) + @user.lat.should == geo.lat + @user.lng.should == geo.lng + end + end + + describe "without profile location data" do + it "should have lat/lng values from ip_address" do + @user.update_attributes({ :city => nil, + :state => nil, + :country => nil, + }) + @user.lat.should == nil + @user.lng.should == nil + geo = JamRuby::MaxMindGeo.ip_lookup('1.1.0.0') + geo.should_not be_nil + geo = JamRuby::MaxMindGeo.ip_lookup('1.1.0.255') + geo.should_not be_nil + @user.update_lat_lng('1.1.0.255') + @user.lat.should == geo.lat + @user.lng.should == geo.lng + end + end + +end diff --git a/ruby/spec/jam_ruby/models/user_search_spec.rb b/ruby/spec/jam_ruby/models/user_search_spec.rb index 486b955b2..c45b37200 100644 --- a/ruby/spec/jam_ruby/models/user_search_spec.rb +++ b/ruby/spec/jam_ruby/models/user_search_spec.rb @@ -5,7 +5,7 @@ describe User do before(:each) do @user = FactoryGirl.create(:user, first_name: "Example", last_name: "User", email: "user@example.com", password: "foobar", password_confirmation: "foobar", musician: true, email_confirmed: true, - city: "Apex", state: "NC", country: "USA") + city: "Apex", state: "NC", country: "US") end it "should allow search of one user" do @@ -54,7 +54,7 @@ describe User do it "should tokenize correctly" do @user2 = FactoryGirl.create(:user, first_name: "peaches", last_name: "test", email: "peach@example.com", password: "foobar", password_confirmation: "foobar", musician: true, email_confirmed: true, - city: "Apex", state: "NC", country: "USA") + city: "Apex", state: "NC", country: "US") ws = User.search("pea") ws.length.should == 1 user_result = ws[0] @@ -64,7 +64,7 @@ describe User do it "users who have signed up, but not confirmed should show up in search index due to VRFS-378" do @user3 = FactoryGirl.create(:user, first_name: "unconfirmed", last_name: "unconfirmed", email: "unconfirmed@example.com", password: "foobar", password_confirmation: "foobar", musician: true, email_confirmed: false, - city: "Apex", state: "NC", country: "USA") + city: "Apex", state: "NC", country: "US") ws = User.search("unconfirmed") ws.length.should == 1 @@ -77,4 +77,4 @@ describe User do user_result = ws[0] user_result.id.should == @user3.id end -end \ No newline at end of file +end diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index df1c01ef7..581ac4d80 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -6,7 +6,7 @@ describe User do before do @user = User.new(first_name: "Example", last_name: "User", email: "user@example.com", - password: "foobar", password_confirmation: "foobar", city: "Apex", state: "NC", country: "USA", terms_of_service: true, musician: true) + password: "foobar", password_confirmation: "foobar", city: "Apex", state: "NC", country: "US", terms_of_service: true, musician: true) @user.musician_instruments << FactoryGirl.build(:musician_instrument, user: @user) end @@ -285,7 +285,7 @@ describe User do end describe "create_dev_user" do - before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "USA", nil, nil) } + before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } subject { @dev_user } @@ -298,7 +298,7 @@ describe User do end describe "updates record" do - before { @dev_user = User.create_dev_user("Seth", "Call2", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "USA", nil, nil) } + before { @dev_user = User.create_dev_user("Seth", "Call2", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } it { should be_valid } diff --git a/web/Gemfile b/web/Gemfile index 80a543947..ab19237e8 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -47,12 +47,15 @@ gem 'aws-sdk', '1.8.0' gem 'aasm', '3.0.16' gem 'carrierwave' gem 'fog' +gem 'unf' #optional fog dependency gem 'devise', '>= 1.1.2' #gem 'thin' # the presence of this gem on mac seems to prevent normal startup of rails. gem 'postgres-copy' #group :libv8 do # gem 'libv8', "~> 3.11.8" #end +gem 'geokit-rails' +gem 'postgres_ext' gem 'quiet_assets', :group => :development diff --git a/web/app/assets/javascripts/bandProfile.js b/web/app/assets/javascripts/bandProfile.js index d03ce3bdf..54d0130d1 100644 --- a/web/app/assets/javascripts/bandProfile.js +++ b/web/app/assets/javascripts/bandProfile.js @@ -245,7 +245,7 @@ $('#band-profile-biography').html(band.biography); } else { - + logger.debug("No band found with bandId = " + bandId); } } @@ -266,7 +266,7 @@ function bindSocial() { // FOLLOWERS - url = "/api/bands/" + bandId + "/followers"; + var url = "/api/bands/" + bandId + "/followers"; $.ajax({ type: "GET", dataType: "json", diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js index fe19afe00..9f697366f 100644 --- a/web/app/assets/javascripts/createSession.js +++ b/web/app/assets/javascripts/createSession.js @@ -178,6 +178,13 @@ function submitForm(evt) { evt.preventDefault(); + // If user hasn't completed FTUE - do so now. + if (!(context.jamClient.FTUEGetStatus())) { + app.afterFtue = function() { submitForm(evt); }; + app.layout.showDialog('ftue'); + return; + } + var isValid = validateForm(); if (!isValid) { // app.notify({ @@ -435,4 +442,4 @@ return this; }; - })(window,jQuery); + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/findMusician.js b/web/app/assets/javascripts/findMusician.js new file mode 100644 index 000000000..03852dc25 --- /dev/null +++ b/web/app/assets/javascripts/findMusician.js @@ -0,0 +1,157 @@ +(function(context,$) { + "use strict"; + + context.JK = context.JK || {}; + context.JK.FindMusicianScreen = function(app) { + + var logger = context.JK.logger; + var musicians = {}; + var musicianList; + + function removeSpinner() { + $('
') + } + + function loadMusicians(queryString) { + addSpinner(); + + // squelch nulls and undefines + queryString = !!queryString ? queryString : ""; + + $.ajax({ + type: "GET", + url: "/api/users?" + queryString, + async: true, + success: afterLoadMusicians, + complete: removeSpinner, + error: app.ajaxError + }); + } + + function search() { + logger.debug("Searching for musicians..."); + clearResults(); + var queryString = 'musicians=1&'; + + // order by + var orderby = $('.musician-order-by').val(); + if (orderby !== null && orderby.length() > 0) { + queryString += "orderby=" + orderby + '&'; + } + // instrument filter + var instrument = $('.instrument-list').val(); + if (instruments !== null && instruments.length() > 0) { + queryString += "instrument=" + instrument; + } + // distance filter + var query_param = $('#musician-query-distance').val(); + if (query_param !== null && query_param.length > 0) { + var matches = query_param.match(/(\d)/); + if (0 < matches.length()) { + var distance = matches[0]; + query_param = $('#musician-query-center').val(); + if (query_param !== null && query_param.length > 0) { + matches = query_param.match(/\\d{5}(-\\d{4})?/); + if (0 < matches.length()) { + var zip = matches[0]; + queryString += "zip=" + query_param + '&'; + queryString += "distance=" + query_param + '&'; + } + } + } + } + loadMusicians(queryString); + } + + function refreshDisplay() { + var priorVisible; + } + + function afterLoadMusicians(musicianList) { + // display the 'no musicians' banner if appropriate + var $noMusiciansFound = $('#musicians-none-found'); + if(musicianList.length == 0) { + $noMusiciansFound.show(); + } + else { + $noMusiciansFound.hide(); + } + + startMusicianLatencyChecks(musicianList); + context.JK.GA.trackFindMusicians(musicianList.length); + } + + /** + * Render a single musician line into the table. + * It will be inserted at the appropriate place according to the + * sortScore in musicianLatency. + */ + function renderMusician(musicianId) { + // store musician in the appropriate bucket and increment category counts + var musician = musicians[musicianId]; + + refreshDisplay(); + } + + function beforeShow(data) { + context.JK.InstrumentSelectorHelper.render('#find-musician-instrument'); + } + + function afterShow(data) { + clearResults(); + refreshDisplay(); + loadMusicians(); + } + + function clearResults() { + musicians = {}; + } + + function events() { + $('#musician-keyword-srch').focus(function() { + $(this).val(''); + }); + + $("#musician-keyword-srch").keypress(function(evt) { + if (evt.which === 13) { + evt.preventDefault(); + search(); + } + }); + $('#btn-refresh').on("click", search); + } + + /** + * Initialize, providing an instance of the MusicianLatency class. + */ + function initialize(latency) { + + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('findMusician', screenBindings); + + musicianList = new context.JK.MusicianList(app); + + events(); + } + + this.initialize = initialize; + this.renderMusician = renderMusician; + this.afterShow = afterShow; + + // Following exposed for easier testing. + this.setMusician = setMusician; + this.clearResults = clearResults; + this.getCategoryEnum = getCategoryEnum; + + return this; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/ftue.js b/web/app/assets/javascripts/ftue.js index 9bb2253b2..21414246b 100644 --- a/web/app/assets/javascripts/ftue.js +++ b/web/app/assets/javascripts/ftue.js @@ -25,6 +25,8 @@ }; var faderMap = { + 'ftue-2-audio-input-fader': jamClient.FTUESetInputVolume, + 'ftue-2-voice-input-fader': jamClient.FTUESetOutputVolume, 'ftue-audio-input-fader': jamClient.FTUESetInputVolume, 'ftue-voice-input-fader': jamClient.FTUESetChatInputVolume, 'ftue-audio-output-fader': jamClient.FTUESetOutputVolume @@ -44,6 +46,10 @@ function beforeShow(data) { var vuMeters = [ + '#ftue-2-audio-input-vu-left', + '#ftue-2-audio-input-vu-right', + '#ftue-2-voice-input-vu-left', + '#ftue-2-voice-input-vu-right', '#ftue-audio-input-vu-left', '#ftue-audio-input-vu-right', '#ftue-voice-input-vu-left', @@ -80,6 +86,8 @@ // Always reset the driver select box to "Choose..." which forces everything // to sync properly when the user reselects their driver of choice. // VRFS-375 and VRFS-561 + $('[layout-wizard="ftue"] [layout-wizard-step="0"] .settings-2-device select').val(""); + $('[layout-wizard="ftue"] [layout-wizard-step="0"] .settings-2-voice select').val(""); $('[layout-wizard="ftue"] [layout-wizard-step="2"] .asio-settings .settings-driver select').val(""); } @@ -317,6 +325,15 @@ $('#asio-framesize').on('change', setAsioFrameSize); $('#asio-input-latency').on('change', setAsioInputLatency); $('#asio-output-latency').on('change', setAsioOutputLatency); + // New FTUE events + $('.ftue-new .settings-2-device select').on('change', newFtueAudioDeviceChanged); + $('.ftue-new .settings-2-voice select').on('change', newFtueAudioDeviceChanged); + $('#btn-ftue-2-asio-resync').on('click', newFtueAsioResync); + $('#btn-ftue-2-asio-control-panel').on('click', openASIOControlPanel); + $('#ftue-2-asio-framesize').on('change', newFtueSetAsioFrameSize); + $('#ftue-2-asio-input-latency').on('change', newFtueSetAsioInputLatency); + $('#ftue-2-asio-output-latency').on('change', newFtueSetAsioOutputLatency); + $('#btn-ftue-2-save').on('click', newFtueSaveSettingsHandler); } /** @@ -368,7 +385,6 @@ * Load available drivers and populate the driver select box. */ function loadAudioDrivers() { - var drivers = jamClient.FTUEGetDevices(); var driverOptionFunc = function(driverKey, index, list) { @@ -377,11 +393,249 @@ }; var optionsHtml = ''; - var $select = $('[layout-wizard-step="2"] .settings-driver select'); - $select.empty(); + var selectors = [ + '[layout-wizard-step="0"] .settings-2-device select', + '[layout-wizard-step="0"] .settings-2-voice select', + '[layout-wizard-step="2"] .settings-driver select' + ]; var sortedDeviceKeys = context._.keys(drivers).sort(); context._.each(sortedDeviceKeys, driverOptionFunc); - $select.html(optionsHtml); + $.each(selectors, function(index, selector) { + var $select = $(selector); + $select.empty(); + $select.html(optionsHtml); + }); + } + + /** + * Handler for the new FTUE save button. + */ + function newFtueSaveSettingsHandler(evt) { + evt.preventDefault(); + var $saveButton = $('#btn-ftue-2-save'); + if ($saveButton.hasClass('disabled')) { + return; + } + var selectedAudioDevice = $('.ftue-new .settings-2-device select').val(); + if (!(selectedAudioDevice)) { + app.notify({ + title: "Please select an audio device", + text: "Please choose a usable audio device, or select cancel." + }); + return false; + } + jamClient.FTUESave(true); + jamClient.FTUESetStatus(true); // No FTUE wizard next time + rest.userCertifiedGear({success:true}); + app.layout.closeDialog('ftue'); + if (app.afterFtue) { + // If there's a function to invoke, invoke it. + app.afterFtue(); + app.afterFtue = null; + } + return false; + } + + // Handler for when the audio device is changed in the new FTUE screen + // This works differently from the old FTUE. There is no input/output selection, + // as soon as the user chooses a driver, we auto-assign inputs and outputs. + // We also call jamClient.FTUEGetExpectedLatency, which returns a structure like: + // { latency: 11.1875, latencyknown: true, latencyvar: 1} + function newFtueAudioDeviceChanged(evt) { + var $select = $(evt.currentTarget); + + var $audioSelect = $('.ftue-new .settings-2-device select'); + var $voiceSelect = $('.ftue-new .settings-2-voice select'); + var audioDriverId = $audioSelect.val(); + var voiceDriverId = $voiceSelect.val(); + jamClient.FTUESetMusicDevice(audioDriverId); + jamClient.FTUESetChatInput(voiceDriverId); + if (voiceDriverId) { // Let the back end know whether a voice device is selected + jamClient.TrackSetChatEnable(true); + } else { + jamClient.TrackSetChatEnable(false); + } + if (!audioDriverId) { + // reset back to 'Choose...' + newFtueEnableControls(false); + return; + } + var musicInputs = jamClient.FTUEGetMusicInputs(); + var musicOutputs = jamClient.FTUEGetMusicOutputs(); + + // set the music input to the first available input, + // and output to the first available output + var kin = null, kout = null, k = null; + // TODO FIXME - this jamClient call returns a dictionary. + // It's difficult to know what to auto-choose. + // For example, with my built-in audio, the keys I get back are + // digital in, line in, mic in and stereo mix. Which should we pick for them? + for (k in musicInputs) { + kin = k; + break; + } + for (k in musicOutputs) { + kout = k; + break; + } + var result; + if (kin && kout) { + jamClient.FTUESetMusicInput(kin); + jamClient.FTUESetMusicOutput(kout); + } else { + // TODO FIXME - how to handle a driver selection where we are unable to + // autoset both inputs and outputs? (I'd think this could happen if either + // the input or output side returned no values) + return; + } + + newFtueEnableControls(true); + newFtueOsSpecificSettings(); + setLevels(0); + newFtueUpdateLatencyView('loading'); + jamClient.FTUESave(false); + setVuCallbacks(); + + var latency = jamClient.FTUEGetExpectedLatency(); + newFtueUpdateLatencyView(latency); + + } + + function newFtueSave(persist) { + newFtueUpdateLatencyView('loading'); + jamClient.FTUESave(persist); + var latency = jamClient.FTUEGetExpectedLatency(); + newFtueUpdateLatencyView(latency); + } + + function newFtueAsioResync(evt) { + // In theory, we should be calling the following, but it causes + // us to not have both inputs/outputs loaded, and simply calling + // FTUE Save appears to resync things. + //jamClient.FTUERefreshDevices(); + newFtueSave(false); + } + + function newFtueSetAsioFrameSize(evt) { + var val = parseFloat($(evt.currentTarget).val(),10); + if (isNaN(val)) { + return; + } + logger.debug("Calling FTUESetFrameSize(" + val + ")"); + jamClient.FTUESetFrameSize(val); + newFtueSave(false); + } + function newFtueSetAsioInputLatency(evt) { + var val = parseInt($(evt.currentTarget).val(),10); + if (isNaN(val)) { + return; + } + logger.debug("Calling FTUESetInputLatency(" + val + ")"); + jamClient.FTUESetInputLatency(val); + newFtueSave(false); + } + function newFtueSetAsioOutputLatency(evt) { + var val = parseInt($(evt.currentTarget).val(),10); + if (isNaN(val)) { + return; + } + logger.debug("Calling FTUESetOutputLatency(" + val + ")"); + jamClient.FTUESetOutputLatency(val); + newFtueSave(false); + } + + // Enable or Disable the frame/buffer controls in the new FTUE screen + function newFtueEnableControls(enable) { + var $frame = $('#ftue-2-asio-framesize'); + var $bin = $('#ftue-2-asio-input-latency'); + var $bout = $('#ftue-2-asio-output-latency'); + if (enable) { + $frame.removeAttr("disabled"); + $bin.removeAttr("disabled"); + $bout.removeAttr("disabled"); + } else { + $frame.attr("disabled", "disabled"); + $bin.attr("disabled", "disabled"); + $bout.attr("disabled", "disabled"); + } + } + + // Based on OS and Audio Hardware, set Frame/Buffer settings appropriately + // and show/hide the ASIO button. + function newFtueOsSpecificSettings() { + var $frame = $('#ftue-2-asio-framesize'); + var $bin = $('#ftue-2-asio-input-latency'); + var $bout = $('#ftue-2-asio-output-latency'); + var $asioBtn = $('#btn-ftue-2-asio-control-panel'); + if (jamClient.GetOSAsString() === "Win32") { + if (jamClient.FTUEHasControlPanel()) { + // Win32 + ControlPanel = ASIO + // frame=2.5, buffers=0 + $asioBtn.show(); + $frame.val(2.5); + $bin.val(0); + $bout.val(0); + } else { + // Win32, no ControlPanel = WDM/Kernel Streaming + // frame=10, buffers=0 + $asioBtn.hide(); + $frame.val(10); + // TODO FIXME - the old FTUE set the buffers to 1 for WDM/Kernel streaming + // The new FTUE spec says to use 0, as I've done here... + $bin.val(0); + $bout.val(0); + } + } else { // Assuming Mac. TODO: Linux check here + // frame=2.5, buffers=0 + $asioBtn.hide(); + $frame.val(2.5); + $bin.val(0); + $bout.val(0); + } + + } + + // Given a latency structure, update the view. + function newFtueUpdateLatencyView(latency) { + var $report = $('.ftue-new .latency .report'); + var $instructions = $('.ftue-new .latency .instructions'); + var latencyClass = "neutral"; + var latencyValue = "N/A"; + var $saveButton = $('#btn-ftue-2-save'); + if (latency && latency.latencyknown) { + latencyValue = latency.latency; + // Round latency to two decimal places. + latencyValue = Math.round(latencyValue * 100) / 100; + if (latency.latency <= 10) { + latencyClass = "good"; + $saveButton.removeClass('disabled'); + } else if (latency.latency <= 20) { + latencyClass = "acceptable"; + $saveButton.removeClass('disabled'); + } else { + latencyClass = "bad"; + $saveButton.addClass('disabled'); + } + } else { + latencyClass = "unknown"; + $saveButton.addClass('disabled'); + } + + $('.ms-label', $report).html(latencyValue); + $('p', $report).html('milliseconds'); + + $report.removeClass('good acceptable bad'); + $report.addClass(latencyClass); + + var instructionClasses = ['neutral', 'good', 'acceptable', 'bad', 'start', 'loading']; + $.each(instructionClasses, function(idx, val) { + $('p.' + val, $instructions).hide(); + }); + if (latency === 'loading') { + $('p.loading', $instructions).show(); + } else { + $('p.' + latencyClass, $instructions).show(); + } } function audioDriverChanged(evt) { @@ -459,6 +713,7 @@ var dialogBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow, 'afterHide': afterHide }; app.bindDialog('ftue', dialogBindings); + app.registerWizardStepFunction("0", settingsInit); app.registerWizardStepFunction("2", settingsInit); app.registerWizardStepFunction("4", testLatency); app.registerWizardStepFunction("6", testComplete); @@ -484,6 +739,8 @@ }; context.JK.ftueAudioInputVUCallback = function(dbValue) { + context.JK.ftueVUCallback(dbValue, '#ftue-2-audio-input-vu-left'); + context.JK.ftueVUCallback(dbValue, '#ftue-2-audio-input-vu-right'); context.JK.ftueVUCallback(dbValue, '#ftue-audio-input-vu-left'); context.JK.ftueVUCallback(dbValue, '#ftue-audio-input-vu-right'); }; @@ -492,6 +749,8 @@ context.JK.ftueVUCallback(dbValue, '#ftue-audio-output-vu-right'); }; context.JK.ftueChatInputVUCallback = function(dbValue) { + context.JK.ftueVUCallback(dbValue, '#ftue-2-voice-input-vu-left'); + context.JK.ftueVUCallback(dbValue, '#ftue-2-voice-input-vu-right'); context.JK.ftueVUCallback(dbValue, '#ftue-voice-input-vu-left'); context.JK.ftueVUCallback(dbValue, '#ftue-voice-input-vu-right'); }; diff --git a/web/app/assets/javascripts/instrumentSelector.js b/web/app/assets/javascripts/instrumentSelector.js new file mode 100644 index 000000000..0c75e8575 --- /dev/null +++ b/web/app/assets/javascripts/instrumentSelector.js @@ -0,0 +1,87 @@ +(function(context,$) { + + /** + * Javascript for managing genre selectors. + */ + + "use strict"; + + context.JK = context.JK || {}; + context.JK.GenreSelectorHelper = (function() { + + var logger = context.JK.logger; + var _genres = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] + + function loadGenres() { + var url = "/api/genres"; + $.ajax({ + type: "GET", + url: url, + async: false, // do this synchronously so the event handlers in events() can be wired up + success: genresLoaded + }); + } + + function reset(parentSelector, defaultGenre) { + defaultGenre = typeof(defaultGenre) == 'undefined' ? '' : defaultGenre; + $('select', parentSelector).val(defaultGenre); + } + + function genresLoaded(response) { + $.each(response, function(index) { + _genres.push({ + value: this.id, + label: this.description + }); + }); + } + + function render(parentSelector) { + $('select', parentSelector).empty(); + $('select', parentSelector).append(''); + var template = $('#template-genre-option').html(); + $.each(_genres, function(index, value) { + // value will be a dictionary entry from _genres: + // { value: xxx, label: yyy } + var genreOptionHtml = context.JK.fillTemplate(template, value); + $('select', parentSelector).append(genreOptionHtml); + }); + } + + function getSelectedGenres(parentSelector) { + var selectedGenres = []; + var selectedVal = $('select', parentSelector).val(); + if (selectedVal !== '') { + selectedGenres.push(selectedVal); + } + return selectedGenres; + } + + function setSelectedGenres(parentSelector, genreList) { + if (!genreList) { + return; + } + var values = []; + $.each(genreList, function(index, value) { + values.push(value.toLowerCase()); + }); + var selectedVal = $('select', parentSelector).val(values); + } + + function initialize() { + loadGenres(); + } + + var me = { // This will be our singleton. + initialize: initialize, + getSelectedGenres: getSelectedGenres, + setSelectedGenres: setSelectedGenres, + reset: reset, + render: render, + loadGenres: loadGenres + }; + + return me; + + })(); +})(window,jQuery); diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index ea66bcbec..341e0d284 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -168,6 +168,14 @@ }); } + function getMusicianFollowers(userId) { + + } + + function getBandFollowers(bandId) { + + } + function getClientDownloads(options) { return $.ajax({ diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 6fade3525..61da06700 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -255,10 +255,6 @@ } logger.debug("Changing screen to " + url); context.location = url; - - if (!(context.jamClient.FTUEGetStatus())) { - app.layout.showDialog('ftue'); - } } this.unloadFunction = function() { @@ -294,6 +290,9 @@ } }; + // Holder for a function to invoke upon successfully completing the FTUE. + // See createSession.submitForm as an example. + this.afterFtue = null; // enable temporary suspension of heartbeat for fine-grained control this.heartbeatActive = true; diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 512fe2484..6958bf426 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -303,7 +303,9 @@ padding: '0px' }; $('[layout]').css(layoutStyle); - $('[layout="notify"]').css({"z-index": "9", "padding": "20px"}); + // JW: Setting z-index of notify to 1001, so it will appear above the dialog overlay. + // This allows dialogs to use the notification. + $('[layout="notify"]').css({"z-index": "1001", "padding": "20px"}); $('[layout="panel"]').css({position: 'relative'}); $('[layout-panel="expanded"] [layout-panel="header"]').css({ margin: "0px", @@ -381,12 +383,20 @@ function linkClicked(evt) { evt.preventDefault(); + var $currentTarget = $(evt.currentTarget); // allow links to be disabled - if($(evt.currentTarget).hasClass("disabled") ) { + if($currentTarget.hasClass("disabled") ) { return; } + // If link requires FTUE, show that first. + if ($currentTarget.hasClass("requires-ftue")) { + if (!(context.jamClient.FTUEGetStatus())) { + app.layout.showDialog('ftue'); + } + } + var destination = $(evt.currentTarget).attr('layout-link'); var destinationType = $('[layout-id="' + destination + '"]').attr("layout"); if (destinationType === "screen") { @@ -595,10 +605,11 @@ } notifyQueue.push({message: message, descriptor: descriptor}); - $notify.slideDown(2000) - .delay(2000) + // JW - speeding up the in/out parts of notify. Extending non-moving time. + $notify.slideDown(250) + .delay(4000) .slideUp({ - duration: 2000, + duration: 400, queue: true, complete: function() { notifyDetails = notifyQueue.shift(); @@ -614,7 +625,7 @@ } } }); - } + }; function setNotificationInfo(message, descriptor) { var $notify = $('[layout="notify"]'); diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index fc69c53bc..17f07bff2 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -307,22 +307,22 @@ $('#profile-location').html(user.location); // stats - var text = user.friend_count > 1 || user.friend_count == 0 ? " Friends" : " Friend"; + var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend"; $('#profile-friend-stats').html(user.friend_count + text); - text = user.follower_count > 1 || user.follower_count == 0 ? " Followers" : " Follower"; + text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower"; $('#profile-follower-stats').html(user.follower_count + text); - text = user.session_count > 1 || user.session_count == 0 ? " Sessions" : " Session"; + text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session"; $('#profile-session-stats').html(user.session_count + text); - text = user.recording_count > 1 || user.recording_count == 0 ? " Recordings" : " Recording"; + text = user.recording_count > 1 || user.recording_count === 0 ? " Recordings" : " Recording"; $('#profile-recording-stats').html(user.recording_count + text); $('#profile-biography').html(user.biography); } else { - + logger.debug("No user found with userId = " + userId); } } diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 3eaf1574e..278f733b8 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -621,8 +621,6 @@ $connection.addClass(connectionClass); } else if (eventName === 'add' || eventName === 'remove') { - //logger.dbg('non-vu event: ' + eventName + ',' + mixerId + ',' + value); - // TODO - _renderSession. Note I get streams of these in // sequence, so have Nat fix, or buffer/spam protect // Note - this is already handled from websocket events. @@ -632,7 +630,7 @@ } else { // Examples of other events // Add media file track: "add", "The_Abyss_4T", 0 - logger.dbg('non-vu event: ' + eventName + ',' + mixerId + ',' + value); + logger.debug('non-vu event: ' + eventName + ',' + mixerId + ',' + value); } } } diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index af030147e..95ac0cde9 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -137,8 +137,16 @@ // wire up the Join Link to the T&Cs dialog var $parentRow = $('tr[id=' + session.id + ']', tbGroup); + $('.join-link', $parentRow).click(function(evt) { - joinClick(session.id); + // If no FTUE, show that first. + if (!(context.jamClient.FTUEGetStatus())) { + app.afterFtue = function() { joinClick(session.id); }; + app.layout.showDialog('ftue'); + return; + } else { + joinClick(session.id); + } }); } } @@ -201,7 +209,7 @@ } function openAlert(sessionId) { - var alertDialog = new context.JK.AlertDialog(app, "YES", + var alertDialog = new context.JK.AlertDialog(app, "YES", "You must be approved to join this session. Would you like to send a request to join?", sessionId, onCreateJoinRequest); @@ -210,7 +218,7 @@ } function sessionNotJoinableAlert() { - var alertDialog = new context.JK.AlertDialog(app, "OK", + var alertDialog = new context.JK.AlertDialog(app, "OK", "This session is over or is no longer public and cannot be joined. Please click Refresh to update the session list.", null, function(evt) { diff --git a/web/app/assets/stylesheets/client/ftue.css.scss b/web/app/assets/stylesheets/client/ftue.css.scss index 9af975c1a..02b4d1f27 100644 --- a/web/app/assets/stylesheets/client/ftue.css.scss +++ b/web/app/assets/stylesheets/client/ftue.css.scss @@ -110,6 +110,155 @@ div.dialog.ftue { margin-top: 12px; } + p.intro { + margin-top:0px; + } + .ftue-new { + clear:both; + position:relative; + width:100%; + height: 54px; + margin-top: 12px; + select { + font-size: 15px; + padding: 3px; + } + .latency { + position: absolute; + top: 120px; + font-size: 12px; + .report { + color:#fff; + position: absolute; + top: 20px; + left: 0px; + width: 105px; + height: 50px; + background-color: #72a43b; + padding: 10px; + text-align: center; + .ms-label { + padding-top: 10px; + font-size: 34px; + font-family: Arial, sans-serif; + } + p { + margin-top: 4px; + } + } + .report.neutral, .report.start, .report.unknown { + background-color: #666; + } + .report.good { + background-color: #72a43b; + } + .report.acceptable { + background-color: #D6A800; + } + .report.bad { + background-color: #7B0C00; + } + .instructions { + color:#fff; + position: absolute; + top: 20px; + left: 125px; + width: 595px; + height: 50px; + padding: 10px; + background-color: #666; + } + .instructions p.start, .instructions p.neutral { + padding-top: 4px; + } + .instructions p.unknown { + margin-top:0px; + padding-top: 4px; + } + .instructions p.good { + padding-top: 4px; + } + .instructions p.acceptable { + margin-top: -6px; + } + .instructions p.bad { + margin-top: -6px; + } + .instructions p a { + font-weight: bold; + } + } + .column { + position:absolute; + width: 220px; + height: 50px; + margin-right:8px; + } + .settings-2-device { + left:0px; + } + .settings-2-center { + left:50%; + margin-left: -110px; + .buttons { + margin-top: 14px; + a { + margin-right: 0px; + } + } + } + .settings-2-voice { + top: 0px; + right:0px; + } + .controls { + margin-top: 16px; + background-color: #222; + height: 48px; + width: 220px; + } + .ftue-vu-left { + position:relative; + top: 0px; + } + .ftue-vu-right { + position:relative; + top: 22px; + } + .ftue-fader { + position:relative; + top: 14px; + left: 8px; + } + .gain-label { + color: $ColorScreenPrimary; + position:absolute; + top: 76px; + right: 6px; + } + + .subcolumn { + position:absolute; + top: 60px; + font-size: 12px !important; + width: 68px; + height: 48px; + } + .subcolumn select { + width: 68px; + } + .subcolumn.first { + left:0px; + } + .subcolumn.second { + left:50%; + margin-left:-34px; + } + .subcolumn.third { + right:0px; + } + } + .asio-settings { clear:both; position:relative; diff --git a/web/app/assets/stylesheets/client/search.css.scss b/web/app/assets/stylesheets/client/search.css.scss index 488e053c0..56cd7368d 100644 --- a/web/app/assets/stylesheets/client/search.css.scss +++ b/web/app/assets/stylesheets/client/search.css.scss @@ -55,3 +55,21 @@ font-size: 90%; } +.query-distance-params { + float:left; + width:140px; + margin-left: 10px; + -webkit-border-radius: 6px; + border-radius: 6px; + background-color:$ColorTextBoxBackground; + border: none; + color:#333; + font-weight:400; + padding:0px 0px 0px 8px; + height:18px; + line-height:18px; + overflow:hidden; + -webkit-box-shadow: inset 2px 2px 3px 0px #888; + box-shadow: inset 2px 2px 3px 0px #888; +} + diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 40a96b869..427a2647a 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -14,8 +14,15 @@ class ApiUsersController < ApiController respond_to :json def index - @users = User.paginate(page: params[:page]) - respond_with @users, responder: ApiResponder, :status => 200 + if 1 == params[:musicians].to_i + query = params.clone + query[:remote_ip] = request.remote_ip + @users = User.musician_search(query, current_user) + respond_with @users, responder: ApiResponder, :status => 200 + else + @users = User.paginate(page: params[:page]) + respond_with @users, responder: ApiResponder, :status => 200 + end end def show diff --git a/web/app/views/clients/_ftue.html.erb b/web/app/views/clients/_ftue.html.erb index ddb4d244b..4084acb51 100644 --- a/web/app/views/clients/_ftue.html.erb +++ b/web/app/views/clients/_ftue.html.erb @@ -7,8 +7,128 @@+ Choose a device to capture and play your session audio. If + you’re not using a mic with this device, then also choose a + voice chat input to talk with others during sessions. Then play + and speak, and adjust the gain faders so that you hear both your + instrument and voice in your headphones at comfortable volumes. +
+ + +Choose an audio device to continue...
+ + + + + +