This commit is contained in:
Seth Call 2013-11-05 03:02:42 +00:00
commit 21b338a5ea
47 changed files with 1301 additions and 118 deletions

View File

@ -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

View File

@ -9,7 +9,7 @@ FactoryGirl.define do
musician true
city "Apex"
state "NC"
country "USA"
country "US"
terms_of_service true

View File

@ -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
add_user_bio.sql
users_geocoding.sql

View File

@ -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;

11
db/up/users_geocoding.sql Normal file
View File

@ -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';

View File

@ -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"

View File

@ -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
end

View File

@ -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

View File

@ -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
end

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
end

View File

@ -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 }

View File

@ -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

View File

@ -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",

View File

@ -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);

View File

@ -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() {
$('<div[layout-id=findMusician] .content .spinner').remove();
}
function addSpinner() {
removeSpinner();
$('<div[layout-id=findMusician] .content').append('<div class="spinner spinner-large"></div>')
}
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);

View File

@ -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 = '<option selected="selected" value="">Choose...</option>';
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');
};

View File

@ -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('<option value="">Select Genre</option>');
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);

View File

@ -168,6 +168,14 @@
});
}
function getMusicianFollowers(userId) {
}
function getBandFollowers(bandId) {
}
function getClientDownloads(options) {
return $.ajax({

View File

@ -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;

View File

@ -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"]');

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -7,8 +7,128 @@
<!-- inner wrapper -->
<div class="ftue-inner" layout-wizard="ftue">
<!-- NEW FTUE first screen, which has latency approximation -->
<!-- Audio device selection and level-setting -->
<div layout-wizard-step="0" dialog-title="audio gear settings" dialog-purpose="GearSettings" style="display:block;">
<p os="win32" class="intro">
Choose a device to capture and play your session audio. If
youre 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.
</p>
<p os="mac" class="intro" style="display:none;">
Choose a device to capture and play your session audio. If
youre 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.
</p>
<div class="ftue-new">
<div class="column settings-2-device">
Session Audio Device:<br />
<select></select>
<!-- VU/Fader for audio device -->
<div class="controls">
<div id="ftue-2-audio-input-vu-left" class="ftue-vu-left"></div>
<div id="ftue-2-audio-input-fader" class="ftue-fader"></div>
<div class="gain-label">GAIN</div>
<div id="ftue-2-audio-input-vu-right" class="ftue-vu-right"></div>
</div>
</div>
<div class="column settings-2-center">
<div class="buttons">
<a class="button-grey" id="btn-ftue-2-asio-resync">
<%= image_tag "content/icon_resync.png", {:width => 12, :height => 14} %>
RESYNC
</a>
<a class="button-grey" id="btn-ftue-2-asio-control-panel">ASIO SETTINGS</a>
</div>
<div class="subcolumn first">
Frame<br />
<select disabled="disabled" id="ftue-2-asio-framesize">
<option value=""></option>
<option value="2.5">2.5</option>
<option value="5">5</option>
<option value="10">10</option>
</select>
</div>
<div class="subcolumn second">
Buffer/In<br />
<select disabled="disabled" id="ftue-2-asio-input-latency">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
</div>
<div class="subcolumn third">
Buffer/Out<br />
<select disabled="disabled" id="ftue-2-asio-output-latency">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
</div>
</div>
<!-- Colum 3: Voice -->
<div class="column settings-2-voice">
Voice Chat Input:<br />
<select></select>
<div class="controls">
<div id="ftue-2-voice-input-vu-left" class="ftue-vu-left"></div>
<div id="ftue-2-voice-input-fader" class="ftue-fader"></div>
<div class="gain-label">GAIN</div>
<div id="ftue-2-voice-input-vu-right" class="ftue-vu-right"></div>
</div>
</div>
<!-- Latency -->
<div class="latency">
Latency:<br />
<div class="report neutral">
<div class="ms-label"></div>
<p></p>
</div>
<div class="instructions">
<p class="start" style="display:block;">Choose an audio device to continue...</p>
<p class="unknown" style="display:none;">Unable to determine latency. Please click <a href="#" layout-wizard-link="2">here</a> to try another test which may be able to determine your audio performance.</p>
<p class="neutral loading" style="display:none;">Estimating latency...</p>
<p class="good" style="display:none;">Your audio speed is good. When done with settings, click Save Settings to continue.</p>
<p class="acceptable" style="display:none;">Your audio speed is acceptable, but borderline. Try setting Frame to 2.5 and Buffers to 0 and see if your audio quality is still OK. When done with settings, click Save Settings to continue. You can also view the <a href="#">Choosing an Audio Device</a> article for information on faster audio devices.</p>
<p class="bad" style="display:none;">We're sorry, but your audio speed is too slow to use JamKazam. Try setting Frame to 2.5 and Buffers to 0 and see if your audio quality is still OK. You can also view the <a href="#">Choosing an Audio Device</a> article for information on faster audio devices.</p>
</div>
</div>
</div>
<div class="right mr30 buttonbar">
<a class="button-grey" layout-action="close">CANCEL</a>
<a class="button-grey">HELP</a>&nbsp;
<a class="button-orange" id="btn-ftue-2-save">SAVE SETTINGS</a>
</div>
</div>
<!-- First screen of the FTUE wizard -->
<div layout-wizard-step="1" dialog-title="welcome!" dialog-purpose="Intro" style="display:block;">
<div layout-wizard-step="1" dialog-title="welcome!" dialog-purpose="Intro" style="display:none;">
<p class="intro">
Please identify which of the three types of audio gear below you
are going to use with the JamKazam service, and click one to

View File

@ -0,0 +1,22 @@
<div style="min-width:770px;">
<div class="left ml35" style="padding-top:3px;">Filter Musician List:</div>
<!-- order by filter -->
<%= select_tag(:musician_order_by, options_for_select(Search::ORDERINGS), {:class => 'musician-order-by'} ) %>
<!-- instrument filter -->
<div id="find-musician-instrument" class="right ml10">
<%= select_tag(:instrument,
options_for_select(['Select Instrument', ''].concat(JamRuby::Instrument.all.collect { |ii| [ii.description, ii.id] })),
{:class => 'instrument-list'} ) %>
</div>
<!-- distance filter -->
<div class="query-distance-params" style="height:25px;">
Within
<input id="musician-query-distance" type="text" name="query-distance" placeholder="100" />
miles of
<input id="musician-query-zip" type="text" name="query-zip" placeholder="zip" />
</div>
<div class="right mr10">
<a id="btn-refresh" href="#/findMusician" style="text-decoration:none;" class="button-grey">REFRESH</a>
</div>
</div>

View File

@ -1,7 +1,7 @@
<!-- Musician Screen -->
<div layout="screen" layout-id="musicians" class="screen secondary">
<div class="content">
<div class="content-head">
<div class="content-icon">
<%= image_tag "content/icon_musicians.png", {:height => 19, :width => 19} %>
</div>
@ -9,5 +9,18 @@
<h1>musicians</h1>
<%= render "screen_navigation" %>
</div>
<p>This feature not yet implemented</p>
<form id="find-musician-form">
<div class="musician-filter">
<%= render :partial => "musician_filter" %>
</div>
<div class="content-scroller">
<div class="content-wrapper" style="padding-left:35px;padding-top:10px;">
</div>
</div>
</form>
<div id="musicians-none-found">
There are no musicians found.
</div>
</div>
</div>

View File

@ -15,7 +15,7 @@ class MaxMindManager < BaseManager
ActiveRecord::Base.connection_pool.with_connection do |connection|
pg_conn = connection.instance_variable_get("@connection")
ip_as_int = ip_address_to_int(ip_address)
pg_conn.exec("SELECT country, region, city FROM max_mind_geo WHERE ip_bottom <= $1 AND ip_top >= $2", [ip_as_int, ip_as_int]) do |result|
pg_conn.exec("SELECT country, region, city FROM max_mind_geo WHERE ip_start <= $1 AND ip_end >= $2", [ip_address, ip_address]) do |result|
if !result.nil? && result.ntuples > 0
country = result.getvalue(0, 0)
state = result[0]['region']
@ -95,10 +95,10 @@ class MaxMindManager < BaseManager
def create_phony_database()
clear_location_table
(0..255).each do |top_octet|
@pg_conn.exec("INSERT INTO max_mind_geo (ip_bottom, ip_top, country, region, city) VALUES ($1, $2, $3, $4, $5)",
@pg_conn.exec("INSERT INTO max_mind_geo (ip_start, ip_end, country, region, city) VALUES ($1, $2, $3, $4, $5)",
[
self.class.ip_address_to_int("#{top_octet}.0.0.0"),
self.class.ip_address_to_int("#{top_octet}.255.255.255"),
"#{top_octet}.0.0.0",
"#{top_octet}.255.255.255",
"US",
"Region #{(top_octet / 2).floor}",
"City #{top_octet}"

View File

@ -26,8 +26,7 @@ describe ApiClaimedRecordingsController do
it "should show the right thing when one recording just finished" do
controller.current_user = @user
get :show, :id => @claimed_recording.id
# puts response.body
response.should be_success
response.should be_success
json = JSON.parse(response.body)
json.should_not be_nil
json["id"].should == @claimed_recording.id

View File

@ -12,7 +12,7 @@ FactoryGirl.define do
musician true
city "Apex"
state "NC"
country "USA"
country "US"
terms_of_service true
subscribe_email true
@ -43,7 +43,7 @@ FactoryGirl.define do
musician false
city "Apex"
state "NC"
country "USA"
country "US"
terms_of_service true
end
@ -88,7 +88,7 @@ FactoryGirl.define do
biography "Established 1978"
city "Apex"
state "NC"
country "USA"
country "US"
end
factory :join_request, :class => JamRuby::JoinRequest do

View File

@ -100,7 +100,7 @@ describe "Band API", :type => :api do
it "should allow band creation" do
last_response = create_band(user, "My Band", "http://www.myband.com", "Bio", "Apex", "NC", "USA", ["country"], "www.photos.com", "www.logos.com")
last_response = create_band(user, "My Band", "http://www.myband.com", "Bio", "Apex", "NC", "US", ["country"], "www.photos.com", "www.logos.com")
last_response.status.should == 201
new_band = JSON.parse(last_response.body)
@ -117,14 +117,14 @@ describe "Band API", :type => :api do
end
it "should prevent bands with less than 1 genre" do
last_response = create_band(user, "My Band", "http://www.myband.com", "Bio", "Apex", "NC", "USA", nil, "www.photos.com", "www.logos.com")
last_response = create_band(user, "My Band", "http://www.myband.com", "Bio", "Apex", "NC", "US", nil, "www.photos.com", "www.logos.com")
last_response.status.should == 400
error_msg = JSON.parse(last_response.body)
error_msg["message"].should == ValidationMessages::GENRE_MINIMUM_NOT_MET
end
it "should prevent bands with more than 1 genre" do
last_response = create_band(user, "My Band", "http://www.myband.com", "Bio", "Apex", "NC", "USA", ["african", "country"], "www.photos.com", "www.logos.com")
last_response = create_band(user, "My Band", "http://www.myband.com", "Bio", "Apex", "NC", "US", ["african", "country"], "www.photos.com", "www.logos.com")
last_response.status.should == 400
error_msg = JSON.parse(last_response.body)
error_msg["message"].should == ValidationMessages::GENRE_LIMIT_EXCEEDED
@ -145,7 +145,7 @@ describe "Band API", :type => :api do
band.genres.size.should == 1
last_response = update_band(user, band.id, "Brian's Band", "http://www.briansband.com", "Bio", "Apex", "NC", "USA", ["african"], "www.photos.com", "www.logos.com")
last_response = update_band(user, band.id, "Brian's Band", "http://www.briansband.com", "Bio", "Apex", "NC", "US", ["african"], "www.photos.com", "www.logos.com")
last_response.status.should == 200
updated_band = JSON.parse(last_response.body)
@ -319,4 +319,4 @@ describe "Band API", :type => :api do
end
end
end
end
end

View File

@ -22,8 +22,8 @@ describe "Search API", :type => :api do
it "simple search" do
@musician = FactoryGirl.create(:user, first_name: "Peach", last_name: "Nothing", email: "user@example.com", musician: true)
@fan = FactoryGirl.create(:user, first_name: "Peach Peach", last_name: "Grovery", email: "fan@example.com", musician: false)
@band = 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", "www.bands2.com", "zomg we rock", "Apex", "NC", "USA", ["hip hop"], user.id, nil, nil)
@band = Band.save(nil, "Peach pit", "www.bands.com", "zomg we rock", "Apex", "NC", "US", ["hip hop"], user.id, nil, nil)
@band2 = Band.save(nil, "Peach", "www.bands2.com", "zomg we rock", "Apex", "NC", "US", ["hip hop"], user.id, nil, nil)
get '/api/search.json?query=peach'
last_response.status.should == 200

View File

@ -35,6 +35,8 @@ gem 'devise'
gem 'postgres-copy'
gem 'aws-sdk'
gem 'bugsnag'
gem 'geokit-rails'
gem 'postgres_ext'
group :development do
gem 'pry'

View File

@ -437,7 +437,6 @@ module JamWebsockets
reconnect_music_session_id = login.client_id if login.value_for_tag(5)
@log.info("*** handle_login: token=#{token}; client_id=#{client_id}")
connection = nil
reconnected = false
# you don't have to supply client_id in login--if you don't, we'll generate one
@ -445,9 +444,26 @@ module JamWebsockets
# give a unique ID to this client. This is used to prevent session messages
# from echoing back to the sender, for instance.
client_id = UUIDTools::UUID.random_create.to_s
else
end
user = valid_login(username, password, token, client_id)
connection = JamRuby::Connection.find_by_client_id(client_id)
# if this connection is reused by a different user, then whack the connection
# because it will recreate a new connection lower down
if !connection.nil? && !user.nil? && connection.user != user
@log.debug("user #{user.email} took client_id #{client_id} from user #{connection.user.email}")
connection.delete
connection = nil
end
client.client_id = client_id
if !user.nil?
@log.debug "user #{user} logged in with client_id #{client_id}"
# check if there's a connection for the client... if it's stale, reconnect it
if connection = JamRuby::Connection.find_by_client_id(client_id)
unless connection.nil?
# FIXME: I think connection table needs to updated within connection_manager
# otherwise this would be 1 line of code (connection.connect!)
@ -467,21 +483,10 @@ module JamWebsockets
end if connection.stale?
end
# if there's a client_id but no connection object, create new client_id
client_id = UUIDTools::UUID.random_create.to_s if !connection
end
client.client_id = client_id
user = valid_login(username, password, token, client_id)
if !user.nil?
@log.debug "user #{user} logged in"
# respond with LOGIN_ACK to let client know it was successful
remote_ip = extract_ip(client)
@semaphore.synchronize do
# remove from pending_queue
@pending_clients.delete(client)
@ -696,7 +701,7 @@ module JamWebsockets
@@log.debug "publishing to session:#{music_session_id} client:#{client_id} from client:#{sender_client_id}"
# put it on the topic exchange3 for clients
self.class.client_exchange.publish(client_msg, :routing_key => "client.#{music_session_id}")
self.class.client_exchange.publish(client_msg, :routing_key => "client.#{client_id}")
end
end
end

View File

@ -9,7 +9,7 @@ FactoryGirl.define do
musician true
city "Apex"
state "NC"
country "USA"
country "US"
terms_of_service true