merge develop

This commit is contained in:
Brian Smith 2014-08-04 22:38:59 -04:00
commit 199ef029bc
100 changed files with 1799 additions and 775 deletions

View File

@ -13,7 +13,7 @@ ActiveAdmin.register_page "Download CSV" do
end
start_time = "#{start_time}"
if end_time.blank?
end_time = Time.now + 1.days
end_time = (Time.now + 1.days).strftime('%F')
else
end_time = "#{end_time}"
end

View File

@ -2,6 +2,7 @@ ActiveAdmin.register JamRuby::ScoreHistory, :as => 'Score History' do
menu :parent => 'Score'
config.batch_actions = false
config.sort_order = 'score_dt_desc'
config.clear_action_items!
config.filters = true
config.per_page = 100

View File

@ -1,9 +1,9 @@
<%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_batch_emails_path : "/admin/batch_emails/#{resource.id}") do |f| %>
<%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_batch_emails_path : "#{Gon.global.prefix}/admin/batch_emails/#{resource.id}") do |f| %>
<%= f.inputs do %>
<%= f.input(:from_email, :label => "From Email", :input_html => {:maxlength => 64}) %>
<%= f.input(:subject, :label => "Subject", :input_html => {:maxlength => 128}) %>
<%= f.input(:test_emails, :label => "Test Emails", :input_html => {:maxlength => 1024, :size => '3x3'}) %>
<%= f.input(:body, :label => "Body", :input_html => {:maxlength => 3096, :size => '10x20'}) %>
<%= f.input(:body, :label => "Body", :input_html => {:maxlength => 60000, :size => '10x20'}) %>
<% end %>
<%= f.actions %>
<% end %>

View File

@ -198,4 +198,7 @@ connection_allow_null_locidispid.sql
track_user_in_scores.sql
median_aggregate.sql
current_scores_use_median.sql
current_scores_ams_index_sms_index_use_user_instrument.sql
locidispid_in_score_histories.sql
define_environment_in_db.sql
drop_session_invite_constraint.sql

View File

@ -0,0 +1,163 @@
-- this adds the user's latency, if available
DROP VIEW current_scores;
CREATE OR REPLACE VIEW current_scores AS
SELECT * FROM (SELECT * , row_number() OVER (PARTITION BY alocidispid, blocidispid, scorer ORDER BY full_score DESC) AS pcnum FROM
(SELECT * FROM
(SELECT percent_rank() over (PARTITION BY alocidispid, blocidispid ORDER BY full_score ASC) AS pc, * FROM
(SELECT tmp.*, (COALESCE(a_users.last_jam_audio_latency, 13) + COALESCE(b_users.last_jam_audio_latency, 13) + tmp.score) AS full_score, a_users.last_jam_audio_latency AS a_audio_latency, b_users.last_jam_audio_latency AS b_audio_latency FROM
(SELECT *, row_number() OVER (PARTITION BY alocidispid, blocidispid ORDER BY scores.created_at DESC) AS rownum FROM scores) tmp
LEFT JOIN users as a_users ON a_users.id = tmp.auserid
LEFT JOIN users as b_users ON b_users.id = tmp.buserid
WHERE rownum < 6) AS score_ranked)
AS tmp2 WHERE pc <= .5 ORDER BY pc DESC) pcs )
AS final WHERE pcnum < 2;
-- check that the music_sessions does not currently have an active_music_sessions
CREATE OR REPLACE FUNCTION sms_index (my_user_id VARCHAR, my_locidispid BIGINT, my_audio_latency INTEGER) RETURNS VOID STRICT VOLATILE AS $$
BEGIN
-- output table to hold tagged music sessions with latency
CREATE TEMPORARY TABLE sms_music_session_tmp (music_session_id VARCHAR(64) NOT NULL, tag INTEGER, latency INTEGER) ON COMMIT DROP;
-- populate sms_music_session_tmp as all music sessions
-- XXX: we should pass in enough info to match pagination/query to reduce the impact of this step
INSERT INTO sms_music_session_tmp SELECT DISTINCT id, NULL::INTEGER AS tag, NULL::INTEGER AS latency
FROM music_sessions
WHERE (scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute')))
AND canceled = FALSE
AND id NOT IN (SELECT id FROM active_music_sessions);
-- tag accepted rsvp as 1
UPDATE sms_music_session_tmp q SET tag = 1 FROM rsvp_slots s, rsvp_requests_rsvp_slots rrs, rsvp_requests r WHERE
q.music_session_id = s.music_session_id AND
s.id = rrs.rsvp_slot_id AND
rrs.rsvp_request_id = r.id AND
r.user_id = my_user_id AND
rrs.chosen = TRUE AND
q.tag is NULL;
-- tag invitation as 2
UPDATE sms_music_session_tmp q SET tag = 2 FROM invitations i WHERE
q.music_session_id = i.music_session_id AND
i.receiver_id = my_user_id AND
q.tag IS NULL;
-- musician access as 3
UPDATE sms_music_session_tmp q SET tag = 3 FROM music_sessions m WHERE
q.music_session_id = m.id AND
m.open_rsvps = TRUE AND
q.tag IS NULL;
-- delete anything not tagged
DELETE FROM sms_music_session_tmp WHERE tag IS NULL;
-- output table to hold users involved in the sms_music_session_tmp sessions and their latency
CREATE TEMPORARY TABLE sms_users_tmp (music_session_id VARCHAR(64), user_id VARCHAR(64) NOT NULL, latency INTEGER) ON COMMIT DROP;
IF my_audio_latency > -1 THEN
-- populate sms_users_tmp with users that have an approved RSVP for sessions in the sms_music_session_tmp table, accompanied with full latency and music session
INSERT INTO sms_users_tmp SELECT q.music_session_id, users.id, s.full_score/2 AS latency
FROM sms_music_session_tmp q
INNER JOIN rsvp_slots ON rsvp_slots.music_session_id = q.music_session_id
INNER JOIN rsvp_requests_rsvp_slots ON rsvp_requests_rsvp_slots.rsvp_slot_id = rsvp_slots.id
INNER JOIN rsvp_requests ON rsvp_requests.id = rsvp_requests_rsvp_slots.rsvp_request_id
INNER JOIN users ON rsvp_requests.user_id = users.id
LEFT OUTER JOIN current_scores s ON s.alocidispid = users.last_jam_locidispid
WHERE
s.blocidispid = my_locidispid AND
rsvp_requests_rsvp_slots.chosen = TRUE;
-- populate sms_users_tmp with invited users for session in the sms_music_session_tmp table, accompanied with full latency and music session
-- specify NULL for music_session_id, because we don't want RSVP users to affect the AVG computed for each session later
INSERT INTO sms_users_tmp SELECT NULL, users.id, s.full_score/2 AS latency
FROM sms_music_session_tmp q
INNER JOIN invitations ON invitations.music_session_id = q.music_session_id
INNER JOIN users ON invitations.receiver_id = users.id
LEFT OUTER JOIN current_scores s ON s.alocidispid = users.last_jam_locidispid
WHERE
s.blocidispid = my_locidispid AND
users.id NOT IN (SELECT user_id FROM sms_users_tmp);
END IF;
-- calculate the average latency
UPDATE sms_music_session_tmp q SET latency = (select AVG(u.latency) FROM sms_users_tmp u WHERE
q.music_session_id = u.music_session_id);
RETURN;
END;
$$ LANGUAGE plpgsql;
-- my_audio_latency can have a special value of -1, which means 'unknown'.
CREATE OR REPLACE FUNCTION ams_index (my_user_id VARCHAR, my_locidispid BIGINT, my_audio_latency INTEGER) RETURNS VOID STRICT VOLATILE AS $$
BEGIN
-- output table to hold tagged music sessions with latency
CREATE TEMPORARY TABLE ams_music_session_tmp (music_session_id VARCHAR(64) NOT NULL, tag INTEGER, latency INTEGER) ON COMMIT DROP;
-- populate ams_music_session_tmp as all music sessions
INSERT INTO ams_music_session_tmp SELECT DISTINCT id, NULL::INTEGER AS tag, NULL::INTEGER AS latency
FROM active_music_sessions;
-- TODO worry about active music session where my_user_id is the creator?
-- eh, maybe, but if the music session is active and you're the creator wouldn't you already be in it?
-- so maybe you're on another computer, so why care? plus seth is talking about auto rsvp'ing the session
-- for you, so maybe not a problem.
-- tag accepted rsvp as 1
UPDATE ams_music_session_tmp q SET tag = 1 FROM rsvp_slots s, rsvp_requests_rsvp_slots rrs, rsvp_requests r WHERE
q.music_session_id = s.music_session_id AND
s.id = rrs.rsvp_slot_id AND
rrs.rsvp_request_id = r.id AND
r.user_id = my_user_id AND
rrs.chosen = TRUE AND
q.tag is NULL;
-- tag invitation as 2
UPDATE ams_music_session_tmp q SET tag = 2 FROM invitations i WHERE
q.music_session_id = i.music_session_id AND
i.receiver_id = my_user_id AND
q.tag IS NULL;
-- musician access as 3
UPDATE ams_music_session_tmp q SET tag = 3 FROM music_sessions m WHERE
q.music_session_id = m.id AND
m.musician_access = TRUE AND
q.tag IS NULL;
-- delete anything not tagged
DELETE FROM ams_music_session_tmp WHERE tag IS NULL;
-- output table to hold users involved in the ams_music_session_tmp sessions and their latency
CREATE TEMPORARY TABLE ams_users_tmp (music_session_id VARCHAR(64), user_id VARCHAR(64) NOT NULL, latency INTEGER) ON COMMIT DROP;
IF my_audio_latency > -1 THEN
-- populate ams_users_tmp with users that have a connection for sessions in the ams_music_session_tmp table, accompanied with full latency and music session
INSERT INTO ams_users_tmp SELECT c.music_session_id, c.user_id, s.full_score/2 AS latency
FROM ams_music_session_tmp q
INNER JOIN connections c ON c.music_session_id = q.music_session_id
LEFT OUTER JOIN current_scores s ON s.alocidispid = c.locidispid
WHERE s.blocidispid = my_locidispid;
-- populate ams_users_tmp with users that have an approved RSVP for sessions inthe ams_music_session_tmp table, accompanied with full latency and music session
-- specify NULL for music_session_id, because we don't want RSVP users to affect the AVG computed for each session later
INSERT INTO ams_users_tmp SELECT NULL, users.id, s.full_score/2 AS latency
FROM ams_music_session_tmp q
INNER JOIN rsvp_slots ON rsvp_slots.music_session_id = q.music_session_id
INNER JOIN rsvp_requests_rsvp_slots ON rsvp_requests_rsvp_slots.rsvp_slot_id = rsvp_slots.id
INNER JOIN rsvp_requests ON rsvp_requests.id = rsvp_requests_rsvp_slots.rsvp_request_id
INNER JOIN users ON rsvp_requests.user_id = users.id
LEFT OUTER JOIN current_scores s ON s.alocidispid = users.last_jam_locidispid
WHERE
s.blocidispid = my_locidispid AND
rsvp_requests_rsvp_slots.chosen = TRUE AND
users.id NOT IN (SELECT user_id FROM ams_users_tmp);
END IF;
-- calculate the average latency
UPDATE ams_music_session_tmp q SET latency = (select AVG(u.latency) FROM ams_users_tmp u WHERE
q.music_session_id = u.music_session_id);
RETURN;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,3 @@
-- https://jamkazam.atlassian.net/browse/VRFS-1951
-- we need to know the environment in the database for advanced behaviors
ALTER TABLE generic_state ADD COLUMN env VARCHAR(255) NOT NULL DEFAULT 'development';

View File

@ -0,0 +1,5 @@
-- https://jamkazam.atlassian.net/browse/VRFS-1968
-- store both locids in score_histories
ALTER TABLE score_histories ADD COLUMN from_locidispid BIGINT NOT NULL;
ALTER TABLE score_histories ADD COLUMN to_locidispid BIGINT NOT NULL;

View File

@ -86,10 +86,10 @@ CREATE OR REPLACE FUNCTION generate_scores_dataset () RETURNS VOID STRICT VOLATI
INSERT INTO cities (city, region, countrycode) select distinct city, region, countrycode from geoiplocations where length(city) > 0 and length(countrycode) > 0;
DELETE FROM regions;
INSERT INTO regions (region, countrycode) select distinct region, countrycode from cities;
INSERT INTO regions (region, regionname, countrycode) select distinct region, region, countrycode from cities;
DELETE FROM countries;
INSERT INTO countries (countrycode) select distinct countrycode from regions;
INSERT INTO countries (countrycode, countryname) select distinct countrycode, countrycode from regions;
END IF;
RETURN;

View File

@ -20,6 +20,9 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
Capybara.run_server = false
end
before { puts "\n" }
after { puts "\n\n" }
TestUser = Class.new do
attr_accessor :email, :password, :first_name, :last_name, :id
@ -46,6 +49,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
it "is possible for #{user1} to sign in and not get disconnected within 30 seconds" do
in_client(user1) do
puts "\n *** #{user1}'s client *** \n"
sign_in_poltergeist user1
repeat_for(30.seconds) do
expect(page).to_not have_selector('.no-websocket-connection') #looks for reconnect dialog every 1 second
@ -57,6 +61,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
# this example heavily based on text_message_spec.rb in 'web'
in_client(user1) do
puts "\n *** #{user1}'s client *** \n"
sign_in_poltergeist(user1)
end
@ -65,6 +70,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
test_goodbye = "#{SecureRandom.uuid} - OK bye!"
in_client(user2) do
puts "\n *** #{user2}'s client *** \n"
sign_in_poltergeist(user2)
expect(page).to have_xpath(
"//div[@class='friend-name' and @user-id='#{user1.id}']/span[@class='friend-status']",
@ -78,6 +84,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
end
in_client(user1) do
puts "\n *** #{user1}'s client *** \n"
expect(page).to have_xpath(
"//div[@class='friend-name' and @user-id='#{user2.id}']/span[@class='friend-status']",
:text => "Available" )
@ -88,12 +95,13 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe
end
in_client(user2) do
puts "\n *** #{user2}'s client *** \n"
find('.previous-message-text', text: test_response)
send_text_message(test_goodbye, close_on_send: true)
end
in_client(user1) { sign_out_poltergeist }
in_client(user2) { sign_out_poltergeist }
in_client(user1) { puts "\n *** #{user1}'s client *** \n"; sign_out_poltergeist }
in_client(user2) { puts "\n *** #{user2}'s client *** \n"; sign_out_poltergeist }
end
let(:queue_limit) { 5 }

View File

@ -34,11 +34,11 @@ RSpec.configure do |config|
end
end
#Capybara.register_driver :poltergeist do |app|
# Capybara::Poltergeist::Driver.new(app, { phantomjs_logger: File.open('console.log', 'w') })
#end
Capybara.javascript_driver = :poltergeist
Capybara.default_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, { :timeout=>120, js_errors: false })
end
#Capybara.javascript_driver = :poltergeist
#Capybara.default_driver = :poltergeist
Capybara.run_server = false # since we're testing an app outside this project
Capybara.default_wait_time = 15 # ^^ ditto

View File

@ -320,6 +320,7 @@ message SessionJoin {
optional string photo_url = 2;
optional string msg = 3;
optional int32 track_changes_counter = 4;
optional string source_user_id = 5;
}
message SessionDepart {

View File

@ -3,35 +3,90 @@
<p>Hello <%= @user.first_name %> --
</p>
<p>The following new sessions that that have been posted during the last 24 hours:
</p>
<ol>
<li>Need someone who plays an instrument that you play</li>
<li>Were posted by someone to whom you have either a good or medium latency connection</li>
</ol>
<p>The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.</p>
<p>Take a look through these new sessions below, and just click the RSVP button on the far right side of the row for any session in which you'd like to play. This will let the session organizer know you're interested, and you'll be notified if the session organizer accepts your request to play in that session!
</p>
<style>
#scheduled-sessions {
margin-top:15px;
width:98%;
font-size:11px;
color:#fff;
background-color:#262626;
border-color:#cccccc;
border-width:1px 0;
border-style:solid;
}
<table style="margin-top:6px; width:98%; font-size:11px; color:#fff; background-color:#262626; border:solid 1px #4d4d4d;" cellspacing="0" cellpadding="0" border="0">
#scheduled-sessions td {
padding:10px 2px;
}
#scheduled-sessions th {
padding:2px;
}
#scheduled-sessions thead {
margin-bottom: 5px;
}
#scheduled-sessions thead th {
border-width:0 0 1px 0;
border-style:solid;
border-color:#cccccc;
}
#scheduled-sessions tbody td {
border-width:1px 0 0 0;
border-style:solid;
border-color:#4d4d4d;
vertical-align:top;
}
#scheduled-sessions span.latency img {
margin-top:2px;
}
</style>
<table id="scheduled-sessions" cellspacing="0" cellpadding="0" border="0">
<!-- header -->
<thead>
<tr>
<th align="left" width="20%">GENRE</th>
<th align="left" width="60%">DESCRIPTION</th>
<th align="left" width="15%">GENRE</th>
<th align="left" width="20%">NAME</th>
<th align="left" width="45%">DESCRIPTION</th>
<th width="20%" style="text-align:center">LATENCY</th>
</tr>
<!-- session row goes here -->
<% @sessions_and_latency.each do |sess| %>
<tr>
<td><%= sess.genre.description %></td>
<td><%= sess.description %></td>
<td style="text-align:center"><%= sess.latency %></td>
</tr>
<% end %>
</thead>
<tbody>
<!-- session row goes here -->
<% @sessions_and_latency.each do |sess| %>
<tr>
<td><%= sess.genre.description %></td>
<td>
<%= sess.name %><br/>
<a href="<%= "http://www.jamkazam.com/sessions/#{sess.id}/details" %>">Details</a>
</td>
<td><%= sess.description %></td>
<td style="text-align:center">
<span class="latency">
<span class="latency-value"><%= (sess.latency / 2).round %> ms</span>
<% if sess.latency <= (APP_CONFIG.max_good_full_score * 2) %>
<%= image_tag("http://www.jamkazam.com/assets/content/icon_green_score.png", alt: 'good score icon') %>
<% else %>
<%= image_tag("http://www.jamkazam.com/assets/content/icon_yellow_score.png", alt: 'fair score icon') %>
<% end %>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: <a href="http://www.jamkazam.com/client#/findSession">http://www.jamkazam.com/client#/findSession</a>.
</p>
<p>To see ALL the scheduled sessions that you might be interested in joining, view our <a href="http://www.jamkazam.com/client#/findSession">Find Session page</a>.</p>
<p>Best Regards,</p>

View File

@ -2,16 +2,11 @@
Hello <%= @user.first_name %> --
The following new sessions that that have been posted during the last 24 hours:
The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.
1. Need someone who plays an instrument that you play
2. Were posted by someone to whom you have either a good or medium latency connection
Take a look through these new sessions below, and just click the RSVP button on the far right side of the row for any session in which you'd like to play. This will let the session organizer know you're interested, and you'll be notified if the session organizer accepts your request to play in that session!
GENRE | DESCRIPTION | LATENCY
GENRE | NAME | DESCRIPTION | LATENCY
<% @sessions_and_latency.each do |sess| %>
<%= sess.genre.description %> | <%= sess.description %> | <%= sess.latency %>
<%= sess.genre.description %> | <%= sess.name %> | <%= sess.description %> | <%= sess.latency %> ms
<% end %>
To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: http://www.jamkazam.com/client#/findSession.

View File

@ -9,7 +9,6 @@
margin-bottom:0px;
line-height:140%;
}
</style>
</head>

View File

@ -54,5 +54,35 @@ module JamRuby
GeoIpBlocks.connection.execute "CREATE TABLE #{copied_name} (LIKE #{table_name} INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING COMMENTS INCLUDING STORAGE)"
copied_name
end
def self.dump(query)
result = ActiveRecord::Base.connection.execute(query)
fields = result.fields
header = ""
fields.each do |field|
header << field
header << ', '
end
header = header[0..header.length - 3]
puts header
result.each do |row|
row_out = ''
fields.each do |field|
if row[field].nil?
row_out << 'NULL'
else
if block_given?
row_out << yield(field, row[field]).to_s
else
row_out << row[field].to_s
end
end
row_out << ', '
end
row_out = row_out[0..row_out.length - 3]
puts row_out
end
end
end
end

View File

@ -395,10 +395,11 @@ module JamRuby
)
end
def session_join(session_id, photo_url, msg, track_changes_counter)
def session_join(session_id, photo_url, source_user_id, msg, track_changes_counter)
join = Jampb::SessionJoin.new(
:session_id => session_id,
:photo_url => photo_url,
:source_user_id => source_user_id,
:msg => msg,
:track_changes_counter => track_changes_counter
)

View File

@ -450,7 +450,7 @@ module JamRuby
[music_sessions, user_scores]
end
def self.participant_create user, music_session_id, client_id, as_musician, tracks
def self.participant_create user, music_session_id, client_id, as_musician, tracks, audio_latency
music_session = MusicSession.find(music_session_id)
if music_session.active_music_session
@ -462,7 +462,7 @@ module JamRuby
active_music_session.with_lock do # VRFS-1297
active_music_session.tick_track_changes
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, 10)
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency)
if connection.errors.any?
# rollback the transaction to make sure nothing is disturbed in the database
@ -519,7 +519,7 @@ module JamRuby
# auto-join this user into the newly created session
as_musician = true
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, 10)
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency)
unless connection.errors.any?
user.update_progression_field(:first_music_session_at)

View File

@ -164,7 +164,7 @@ module JamRuby
# if user joins the session as a musician, update their addr and location
if as_musician
user.update_addr_loc(self, User::JAM_REASON_JOIN)
user.update_audio_latency(self, audio_latency)
user.update_audio_latency(self, audio_latency) if audio_latency # try not to let a previously recorded value get nil'ed
end
end

View File

@ -19,16 +19,45 @@ module JamRuby
def persisted?; false; end
end
# Temporary Tables created by this class:
# tmp_candidate_sessions
# ----------------------
#
# These are 'open' sessions that have any open slots left, and fall within a certain start time.
# The session creator must also have a locidispid.
#
# session_id - music_session.id
# creator_id - music_session.user_id
# creator_score_idx - this is the creator's users.last_jam_locidispid
# instrument_id - instruments that are open as gleamed from the RSVP. If this is NULL, it means 'ANY INSTRUMENT'
# invited_user_id - the ID of a user who was invited. Can be NULL.
#
# tmp_candidate_recipients
# ------------------------
#
# These are musicians, that allow email notifications, that have an instrument which matches the session's open RSVP slot's instrument.
# The musician must also have a locidispid.
#
# receiver_id - user ID that could be in the session
# receiver_score_idx - the user's last_jam_locidispid
# instrument_id - the user's matching instrument for a open session slot. If this is NULL, it means 'ANY INSTRUMENT'
# invited_user_id
#
# tmp_matches
# -----------
#
# These are 'candidate_recipients' that have a decent enough score with the creator of the music sessions in tmp_candidate_sessions
#
# receiver_id - the user.id that should receive an Daily Session email
# session_id - the music_session.id for the email
# latency - the score.score between the creator and the candidate (needs to be full score soon)
class EmailBatchScheduledSessions < EmailBatchPeriodic
BATCH_SIZE = 500
SINCE_DAYS = 2
MIN_HOURS_START = 2
TMP_SESS = 'tmp_candidate_sessions'
TMP_RECIP = 'tmp_candidate_recipients'
TMP_MATCH = 'tmp_matches'
ENV_MAX_LATENCY = 'env_max_latency'
ENV_QUERY_LIMIT = 'env_query_limit'
SNAPSHOT_QUERY_LIMIT = '500'
@ -57,18 +86,18 @@ module JamRuby
end
def snapshot_eligible_sessions
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM #{TMP_SESS}")
[0 < rr.count ? rr[0]['num'].to_i : 0, ResultStub.stubs("SELECT * FROM #{TMP_SESS}")]
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM tmp_candidate_sessions")
[0 < rr.count ? rr[0]['num'].to_i : 0, ResultStub.stubs("SELECT * FROM tmp_candidate_sessions")]
end
def snapshot_eligible_recipients
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM #{TMP_RECIP}")
[0 < rr.count ? rr[0]['num'].to_i : 0, ResultStub.stubs("SELECT * FROM #{TMP_RECIP}")]
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM tmp_candidate_recipients")
[0 < rr.count ? rr[0]['num'].to_i : 0, ResultStub.stubs("SELECT * FROM tmp_candidate_recipients")]
end
def snapshot_scored_recipients
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM #{TMP_MATCH}")
[0 < rr.count ? rr[0]['num'].to_i : 0, ResultStub.stubs("SELECT * FROM #{TMP_MATCH}")]
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM tmp_matches")
[0 < rr.count ? rr[0]['num'].to_i : 0, ResultStub.stubs("SELECT * FROM tmp_matches")]
end
def take_snapshot
@ -77,6 +106,62 @@ module JamRuby
self.update_attribute(:test_emails, @counters.inspect)
end
def query
ActiveRecord::Base.connection.execute(<<SQL
SELECT DISTINCT
users.id AS receiver_id,
music_sessions.id AS session_id,
current_scores.full_score AS latency
FROM current_scores
INNER JOIN
(SELECT
msess.id AS session_id,
msess.user_id AS creator_id,
users.last_jam_locidispid AS creator_score_idx,
rs.instrument_id,
invitations.receiver_id AS invited_user_id,
msess.is_unstructured_rsvp
FROM music_sessions msess
INNER JOIN users ON users.id = msess.user_id
INNER JOIN rsvp_slots AS rs ON rs.music_session_id = msess.id
LEFT JOIN rsvp_requests_rsvp_slots AS rrrs ON rrrs.rsvp_slot_id = rs.id
LEFT JOIN invitations ON invitations.music_session_id = msess.id
WHERE
open_rsvps = TRUE OR invitations.id IS NOT NULL AND
users.last_jam_locidispid IS NOT NULL AND
msess.created_at > '#{earliest_session_create_time}' AND
msess.created_at < '#{latest_session_create_time}' AND
scheduled_start >= '#{earliest_session_start_time}' AND
(rrrs.rsvp_slot_id IS NULL OR rrrs.chosen != TRUE)) AS tmp_candidate_sessions
INNER JOIN
(SELECT
users.id AS receiver_id,
users.last_jam_locidispid AS receiver_score_idx,
mi.instrument_id
INTO TEMP TABLE tmp_candidate_recipients
FROM users
INNER JOIN musicians_instruments AS mi ON mi.user_id = users.id
INNER JOIN tmp_candidate_sessions ON tmp_candidate_sessions.instrument_id = mi.instrument_id OR
tmp_candidate_sessions.is_unstructured_rsvp = TRUE OR
tmp_candidate_sessions.invited_user_id = users.id
WHERE
users.last_jam_locidispid IS NOT NULL AND
users.musician = TRUE AND
users.subscribe_email = TRUE) AS tmp_candidate_recipients
INNER JOIN tmp_candidate_sessions ON tmp_candidate_sessions.creator_score_idx = current_scores.alocidispid
INNER JOIN tmp_candidate_recipients ON tmp_candidate_recipients.receiver_score_idx = current_scores.blocidispid
WHERE
current_scores.full_score < #{max_score} AND
tmp_candidate_recipients.receiver_id != tmp_candidate_sessions.creator_id
GROUP BY
tmp_candidate_recipients.receiver_id,
tmp_candidate_sessions.session_id,
latency
SQL
)
end
def fetch_recipients(per_page=BATCH_SIZE)
objs = []
@ -91,9 +176,11 @@ module JamRuby
# now just get the sessions/latency for each distinct mail recipient
_select_scored_recipients(offset).each do |result|
receiver = User.find_by_id(result['receiver_id'])
sessions = MusicSession.select("music_sessions.*, #{TMP_MATCH}.latency")
.joins("INNER JOIN #{TMP_MATCH} ON #{TMP_MATCH}.session_id = music_sessions.id")
.where(["#{TMP_MATCH}.receiver_id = ?", receiver.id])
sessions = MusicSession.select("music_sessions.*, tmp_matches.latency")
.joins("INNER JOIN tmp_matches ON tmp_matches.session_id = music_sessions.id")
.where(["tmp_matches.receiver_id = ?", receiver.id])
.order('tmp_matches.latency')
.limit(20)
.includes([:genre, :creator])
block_given? ? yield(receiver, sessions) : objs << [receiver, sessions]
end
@ -126,75 +213,83 @@ module JamRuby
# inserts eligible sessions to temp table
def _collect_eligible_sessions
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{TMP_SESS}")
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS tmp_candidate_sessions")
limit_sql = (self.snapshot? && 0 < ENV[ENV_QUERY_LIMIT].to_i) ? "LIMIT #{ENV[ENV_QUERY_LIMIT]}" : ''
sql =<<SQL
SELECT
msess.id AS session_id,
msess.user_id AS creator_id,
users.last_jam_locidispid AS creator_score_idx,
rs.instrument_id
INTO TEMP TABLE #{TMP_SESS}
rs.instrument_id,
invitations.receiver_id AS invited_user_id,
msess.is_unstructured_rsvp,
msess.open_rsvps
INTO TEMP TABLE tmp_candidate_sessions
FROM music_sessions msess
INNER JOIN users ON users.id = msess.user_id
INNER JOIN rsvp_slots AS rs ON rs.music_session_id = msess.id
LEFT JOIN rsvp_requests_rsvp_slots AS rrrs ON rrrs.rsvp_slot_id = rs.id
LEFT JOIN invitations ON open_rsvps = FALSE AND invitations.music_session_id = msess.id
WHERE
musician_access = 't' AND
approval_required = 'f' AND
(msess.is_unstructured_rsvp = TRUE OR (rrrs.id IS NULL OR rrrs.chosen != TRUE)) AND
users.last_jam_locidispid IS NOT NULL AND
msess.created_at > '#{earliest_session_create_time}' AND
msess.created_at < '#{latest_session_create_time}' AND
scheduled_start >= '#{earliest_session_start_time}' AND
(rrrs.rsvp_slot_id IS NULL OR rrrs.chosen != 't')
scheduled_start >= '#{earliest_session_start_time}'
#{limit_sql}
SQL
ActiveRecord::Base.connection.execute(sql)
end
def _collect_eligible_recipients
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{TMP_RECIP}")
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS tmp_candidate_recipients").check
limit_sql = (self.snapshot? && 0 < ENV[ENV_QUERY_LIMIT].to_i) ? "LIMIT #{ENV[ENV_QUERY_LIMIT]}" : ''
# load eligible recipients into tmp table
sql =<<SQL
SELECT
users.id AS receiver_id,
users.last_jam_locidispid AS receiver_score_idx,
mi.instrument_id
INTO TEMP TABLE #{TMP_RECIP}
mi.instrument_id,
tmp_candidate_sessions.invited_user_id,
tmp_candidate_sessions.session_id AS session_id,
tmp_candidate_sessions.creator_score_idx AS creator_score_idx
INTO TEMP TABLE tmp_candidate_recipients
FROM users
INNER JOIN musicians_instruments AS mi ON mi.user_id = users.id
INNER JOIN #{TMP_SESS} ON #{TMP_SESS}.instrument_id = mi.instrument_id
INNER JOIN tmp_candidate_sessions ON tmp_candidate_sessions.is_unstructured_rsvp = TRUE OR
(tmp_candidate_sessions.open_rsvps = TRUE AND tmp_candidate_sessions.instrument_id = mi.instrument_id) OR
tmp_candidate_sessions.invited_user_id = users.id
WHERE
users.last_jam_locidispid IS NOT NULL AND
users.musician = 't' AND
users.subscribe_email = 't'
users.musician = TRUE AND
users.subscribe_email = TRUE AND
users.id != tmp_candidate_sessions.creator_id
#{limit_sql}
SQL
ActiveRecord::Base.connection.execute(sql)
end
def _collect_scored_recipients
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{TMP_MATCH}")
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS tmp_matches")
if !self.snapshot? || 0 == (max_score = ENV[ENV_MAX_LATENCY].to_i)
max_score = Score::MAX_YELLOW_LATENCY
max_score = APP_CONFIG.max_yellow_full_score * 2
end
limit_sql = (self.snapshot? && 0 < ENV[ENV_QUERY_LIMIT].to_i) ? "LIMIT #{ENV[ENV_QUERY_LIMIT]}" : ''
sql =<<SQL
SELECT
DISTINCT #{TMP_RECIP}.receiver_id,
#{TMP_SESS}.session_id,
scores.score AS latency
INTO TEMP TABLE #{TMP_MATCH}
FROM scores
INNER JOIN #{TMP_SESS} ON #{TMP_SESS}.creator_score_idx = scores.alocidispid
INNER JOIN #{TMP_RECIP} ON #{TMP_RECIP}.receiver_score_idx = scores.blocidispid
SELECT DISTINCT
tmp_candidate_recipients.receiver_id,
tmp_candidate_recipients.session_id,
current_scores.full_score AS latency
INTO TEMP TABLE tmp_matches
FROM current_scores
INNER JOIN tmp_candidate_recipients ON
tmp_candidate_recipients.creator_score_idx = current_scores.alocidispid AND
tmp_candidate_recipients.receiver_score_idx = current_scores.blocidispid
WHERE
scores.score < #{max_score} AND
#{TMP_RECIP}.receiver_id != #{TMP_SESS}.creator_id
GROUP BY
#{TMP_RECIP}.receiver_id,
#{TMP_SESS}.session_id,
current_scores.full_score < #{max_score}
GROUP BY
tmp_candidate_recipients.receiver_id,
tmp_candidate_recipients.session_id,
latency
#{limit_sql}
SQL
@ -204,13 +299,13 @@ SQL
# select recipients whose score is below minimum threshold
def _select_scored_recipients(offset=0)
if 0 > offset
sql = "SELECT COUNT(DISTINCT receiver_id) AS num FROM #{TMP_MATCH}"
sql = "SELECT COUNT(DISTINCT receiver_id) AS num FROM tmp_matches"
rr = ActiveRecord::Base.connection.execute(sql)
return 0 < rr.count ? rr[0]['num'].to_i : 0
else
sql =<<SQL
SELECT DISTINCT receiver_id
FROM #{TMP_MATCH}
FROM tmp_matches
ORDER BY receiver_id ASC
LIMIT #{@per_page}
OFFSET #{offset}
@ -234,11 +329,11 @@ SQL
return @counters if @counters || !self.snapshot?
_load_recipients if load_tmp_tables
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM #{TMP_SESS}")
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM tmp_candidate_sessions")
session_count = 0 < rr.count ? rr[0]['num'].to_i : 0
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM #{TMP_RECIP}")
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM tmp_candidate_recipients")
receiver_candidate_count = 0 < rr.count ? rr[0]['num'].to_i : 0
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM #{TMP_MATCH}")
rr = ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS num FROM tmp_matches")
receiver_match_count = 0 < rr.count ? rr[0]['num'].to_i : 0
@counters = {

View File

@ -4,6 +4,21 @@ module JamRuby
self.table_name = 'generic_state'
validates :env, :inclusion => {:in => ['development', 'staging', 'production', 'test']}
def self.allow_emails?
database_environment = singleton.env
# if the database says we are in production/staging, then the config has to also agree and say we are in production/staging to send emails
# this is to protect against developers loading a staging or production environment and possibly sending emails to
# we even go one step further, and make sure ENV['BUILD_NUMBER'] is set, which is something you do in production, but would be very rare in development
# or if your database says 'development' and config say 'development', then we allow emails to go out too
(!ENV['BUILD_NUMBER'].nil? && (Environment.mode == 'production' || Environment.mode == 'staging') && (database_environment == 'production' || database_environment == 'staging')) ||
(database_environment == 'development' && Environment.mode == 'development')
end
def self.singleton
GenericState.find('default')
end

View File

@ -10,7 +10,7 @@ module JamRuby
RECURRING_MODES = [NO_RECURRING, RECURRING_WEEKLY]
attr_accessor :legal_terms, :language_description, :access_description
attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed
attr_accessor :approved_rsvps, :open_slots, :pending_invitations
@ -58,11 +58,17 @@ module JamRuby
before_create :generate_share_token
before_create :add_to_feed
#before_save :update_scheduled_start
before_save :check_scheduling_info_changed
SHARE_TOKEN_LENGTH = 8
SEPARATOR = '|'
def check_scheduling_info_changed
@scheduling_info_changed = scheduled_start_changed?
true
end
def add_to_feed
feed = Feed.new
feed.music_session = self
@ -111,6 +117,7 @@ module JamRuby
new_slot = RsvpSlot.new
new_slot.instrument_id = slot.instrument_id
new_slot.proficiency_level = slot.proficiency_level
new_slot.is_unstructured_rsvp = slot.is_unstructured_rsvp
new_session.rsvp_slots << new_slot
# get the request for this slot that was approved (should only be ONE)
@ -409,7 +416,7 @@ module JamRuby
end
def has_mount?
active_music_session && active_music_session.mount
!active_music_session.nil? && !active_music_session.mount.nil?
end
def can_cancel? user

View File

@ -492,6 +492,7 @@ module JamRuby
msg = @@message_factory.session_join(
music_session.id,
user.photo_url,
user.id,
notification_msg,
music_session.track_changes_counter
)

View File

@ -253,7 +253,7 @@ module JamRuby
# send notification
if music_session.creator.id == user.id
Notification.send_scheduled_session_rsvp_cancelled_org(music_session, user)
Notification.send_scheduled_session_rsvp_cancelled_org(music_session, rsvp_request.user)
else
Notification.send_scheduled_session_rsvp_cancelled(music_session, user)
end

View File

@ -3,7 +3,7 @@ require 'ipaddr'
module JamRuby
class Score < ActiveRecord::Base
MAX_YELLOW_LATENCY = 40
MAX_YELLOW_LATENCY = 40 # this round-trip internet latency, not full score
self.table_name = 'scores'

View File

@ -15,14 +15,15 @@ module JamRuby
result = connection.execute(
"INSERT INTO score_histories
(from_client_id, from_user_id, from_latency_tester_id, from_addr, from_isp, from_country, from_region, from_city, from_postal, from_latitude, from_longitude,
to_client_id, to_user_id, to_latency_tester_id, to_addr, to_isp, to_country, to_region, to_city, to_postal, to_latitude, to_longitude,
(from_client_id, from_user_id, from_latency_tester_id, from_addr, from_locidispid, from_isp, from_country, from_region, from_city, from_postal, from_latitude, from_longitude,
to_client_id, to_user_id, to_latency_tester_id, to_addr, to_locidispid, to_isp, to_country, to_region, to_city, to_postal, to_latitude, to_longitude,
score, score_dt, scoring_data)
SELECT
s.anodeid AS from_client_id,
s.auserid AS from_user_id,
s.alatencytestid AS from_latency_tester_id,
s.aaddr AS from_addr,
s.alocidispid AS from_locidispid,
x.company AS from_isp,
a.countrycode AS from_country,
a.region AS from_region,
@ -34,6 +35,7 @@ module JamRuby
s.buserid AS to_user_id,
s.blatencytestid AS to_latency_tester_id,
s.baddr AS to_addr,
s.blocidispid AS to_locidispid,
y.company AS to_isp,
b.countrycode AS to_country,
b.region AS to_region,

View File

@ -185,6 +185,7 @@ module JamRuby
score_join = 'left outer' # or 'inner'
score_min = nil
score_max = nil
# these score_min, score_max come from here (doubled): https://jamkazam.atlassian.net/browse/VRFS-1962
case score_limit
when GOOD_SCORE
score_join = 'inner'
@ -193,14 +194,14 @@ module JamRuby
when MODERATE_SCORE
score_join = 'inner'
score_min = 40
score_max = 80
score_max = 70
when POOR_SCORE
score_join = 'inner'
score_min = 80
score_max = 120
score_max = 100
when UNACCEPTABLE_SCORE
score_join = 'inner'
score_min = 120
score_min = 100
score_max = nil
when SCORED_SCORE
score_join = 'inner'
@ -221,11 +222,11 @@ module JamRuby
rel = rel.joins('LEFT JOIN regions ON regions.countrycode = users.country AND regions.region = users.state')
rel = rel.where(['current_scores.score > ?', score_min]) unless score_min.nil?
rel = rel.where(['current_scores.score <= ?', score_max]) unless score_max.nil?
rel = rel.where(['current_scores.full_score > ?', score_min]) unless score_min.nil?
rel = rel.where(['current_scores.full_score <= ?', score_max]) unless score_max.nil?
rel = rel.select('current_scores.score, regions.regionname')
rel = rel.group('current_scores.score, regions.regionname')
rel = rel.select('current_scores.full_score, current_scores.score, current_scores.b_audio_latency as audio_latency, regions.regionname')
rel = rel.group('current_scores.full_score, current_scores.score, current_scores.b_audio_latency, regions.regionname')
end
ordering = self.order_param(params)
@ -249,7 +250,7 @@ module JamRuby
end
unless locidispid.nil?
rel = rel.order('current_scores.score ASC NULLS LAST')
rel = rel.order('current_scores.full_score ASC NULLS LAST')
end
rel = rel.order('users.created_at DESC')
@ -393,15 +394,15 @@ module JamRuby
# an offline process and thus uses the last jam location as "home base"
locidispid = usr.last_jam_locidispid
score_limit = 60
score_limit = 70
limit = 50
rel = User.musicians_geocoded
.where(['users.created_at >= ? AND users.id != ?', since_date, usr.id])
.joins('inner join current_scores on users.last_jam_locidispid = current_scores.alocidispid')
.where(['current_scores.blocidispid = ?', locidispid])
.where(['current_scores.score <= ?', score_limit])
.order('current_scores.score') # best scores first
.where(['current_scores.full_score <= ?', score_limit])
.order('current_scores.full_score') # best scores first
.order('users.created_at DESC') # then most recent
.limit(limit)

View File

@ -27,8 +27,8 @@ module JamRuby
end
def run
# get all weekly sessions that have ended in the last 15 minutes
criteria = "recurring_mode = 'weekly' AND session_removed_at is not null AND canceled = false AND next_session_scheduled = false"
# get all weekly sessions that started at least 4 hours ago
criteria = "recurring_mode = 'weekly' AND scheduled_start + interval '4hours' < NOW() AND canceled = false AND next_session_scheduled = false"
MusicSession.find_each(:conditions => criteria) do |music_session|
music_session.copy
end

View File

@ -2,6 +2,10 @@ require 'faker'
FactoryGirl.define do
factory :user, :class => JamRuby::User do
ignore do
specific_instruments nil
end
sequence(:email) { |n| "person_#{n}@example.com"}
sequence(:first_name) { |n| "Person" }
sequence(:last_name) { |n| "#{n}" }
@ -17,8 +21,14 @@ FactoryGirl.define do
#u.association :musician_instrument, factory: :musician_instrument, user: u
before(:create) do |user|
user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user)
before(:create) do |user, evaluator|
if evaluator.specific_instruments
evaluator.specific_instruments.each do |instrument|
user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user, instrument: instrument)
end
else
user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user)
end
end
factory :fan do

View File

@ -354,8 +354,8 @@ describe ActiveMusicSession do
user = FactoryGirl.create(:user, last_jam_locidispid: 1, last_jam_audio_latency: 5)
c3 = FactoryGirl.create(:connection, user: user, locidispid: 1, last_jam_audio_latency: 5)
Score.createx(c1.locidispid, c1.client_id, c1.addr, c3.locidispid, c3.client_id, c3.addr, 20, nil)
Score.createx(c2.locidispid, c2.client_id, c2.addr, c3.locidispid, c3.client_id, c3.addr, 30, nil)
Score.createx(c1.locidispid, c1.client_id, c1.addr, c3.locidispid, c3.client_id, c3.addr, 20, nil, nil, {auserid: creator.id, buserid: user.id})
Score.createx(c2.locidispid, c2.client_id, c2.addr, c3.locidispid, c3.client_id, c3.addr, 30, nil, nil, {auserid: creator2.id, buserid: user.id})
# make a transaction

View File

@ -10,36 +10,62 @@ describe EmailBatchScheduledSessions do
UserMailer.deliveries.clear
end
def find_user_sessions(obj, user)
found = obj.find{|item| item[0] == user}
found.should_not be_nil
found[1]
end
describe 'daily scheduled' do
# before { pending }
let (:scheduled_batch) { FactoryGirl.create(:email_batch_scheduled_session) }
let (:drums) { FactoryGirl.create(:instrument, :description => 'drums') }
let (:guitar) { FactoryGirl.create(:instrument, :description => 'guitar') }
let (:bass) { FactoryGirl.create(:instrument, :description => 'bass') }
let (:vocals) { FactoryGirl.create(:instrument, :description => 'vocal') }
let (:drums) { Instrument.find('drums') }
let (:guitar) { Instrument.find('acoustic guitar') }
let (:bass) { Instrument.find('bass guitar') }
let (:vocals) { Instrument.find('voice') }
let (:electric_guitar) { Instrument.find('electric guitar')}
let (:drummer) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:guitarist) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:bassist) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:vocalist) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:loser) { FactoryGirl.create(:user, :last_jam_locidispid => 2, :last_jam_addr => 2) }
let (:drummer) { FactoryGirl.create(:user, first_name: 'drummer', :last_jam_locidispid => 1, :last_jam_addr => 1, specific_instruments: [drums, guitar]) }
let (:guitarist) { FactoryGirl.create(:user, first_name: 'guitarist', :last_jam_locidispid => 1, :last_jam_addr => 1, specific_instruments: [guitar, drums]) }
let (:bassist) { FactoryGirl.create(:user, first_name: 'bassist', :last_jam_locidispid => 1, :last_jam_addr => 1, specific_instruments: [bass]) }
let (:vocalist) { FactoryGirl.create(:user, first_name: 'vocalist', :last_jam_locidispid => 1, :last_jam_addr => 1, specific_instruments: [vocals, electric_guitar]) }
let (:loser) { FactoryGirl.create(:user, first_name: 'loser', :last_jam_locidispid => 2, :last_jam_addr => 2, specific_instruments: [vocals, drums]) }
let (:session3_creator) {FactoryGirl.create(:user, first_name: 'session3_creator', :last_jam_locidispid => 3, :last_jam_addr => 3, specific_instruments: [vocals]) }
let (:session4_creator) {FactoryGirl.create(:user, first_name: 'session4_creator', :last_jam_locidispid => 4, :last_jam_addr => 4, specific_instruments: [vocals]) }
let (:session1) do
FactoryGirl.create(:music_session,
:creator => drummer,
:scheduled_start => Time.now() + 2.days,
:musician_access => true,
:approval_required => false,
:open_rsvps=> true,
:is_unstructured_rsvp => false,
:created_at => Time.now - 1.hour)
end
let (:session2) do
FactoryGirl.create(:music_session,
:creator => drummer,
:open_rsvps => false,
:is_unstructured_rsvp => false,
:scheduled_start => Time.now() + 2.days,
:created_at => Time.now - 1.hour)
end
let (:session3) do
FactoryGirl.create(:music_session,
:creator => session3_creator,
:open_rsvps => true,
:is_unstructured_rsvp => true,
:scheduled_start => Time.now() + 2.days,
:created_at => Time.now - 1.hour)
end
let (:session4) do
FactoryGirl.create(:music_session,
:creator => session4_creator,
:open_rsvps => true,
:is_unstructured_rsvp => true,
:scheduled_start => Time.now() + 2.days,
:musician_access => true,
:approval_required => false,
:created_at => Time.now - 1.hour)
end
@ -49,23 +75,9 @@ describe EmailBatchScheduledSessions do
JamRuby::Score.delete_all
scheduled_batch.reset!
drummer.musician_instruments << FactoryGirl.build(:musician_instrument, user: drummer, instrument: drums, proficiency_level: 2)
drummer.musician_instruments << FactoryGirl.build(:musician_instrument, user: drummer, instrument: guitar, proficiency_level: 2)
guitarist.musician_instruments << FactoryGirl.build(:musician_instrument, user: guitarist, instrument: guitar, proficiency_level: 2)
guitarist.musician_instruments << FactoryGirl.build(:musician_instrument, user: guitarist, instrument: bass, proficiency_level: 2)
bassist.musician_instruments << FactoryGirl.build(:musician_instrument, user: bassist, instrument: bass, proficiency_level: 2)
bassist.musician_instruments << FactoryGirl.build(:musician_instrument, user: bassist, instrument: guitar, proficiency_level: 2)
vocalist.musician_instruments << FactoryGirl.build(:musician_instrument, user: vocalist, instrument: vocals, proficiency_level: 2)
loser.musician_instruments << FactoryGirl.build(:musician_instrument, user: loser, instrument: vocals, proficiency_level: 2)
loser.musician_instruments << FactoryGirl.build(:musician_instrument, user: loser, instrument: drums, proficiency_level: 2)
FactoryGirl.create(:rsvp_slot, :instrument => drums, :music_session => session1)
FactoryGirl.create(:rsvp_slot, :instrument => guitar, :music_session => session1)
FactoryGirl.create(:rsvp_slot, :instrument => bass, :music_session => session1)
@drums_rsvp_slot = FactoryGirl.create(:rsvp_slot, :instrument => drums, :music_session => session1)
@guitar_rsvp_slot = FactoryGirl.create(:rsvp_slot, :instrument => guitar, :music_session => session1)
@bass_rsvp_slot = FactoryGirl.create(:rsvp_slot, :instrument => bass, :music_session => session1)
FactoryGirl.create(:rsvp_slot, :instrument => drums, :music_session => session2)
FactoryGirl.create(:rsvp_slot, :instrument => guitar, :music_session => session2)
FactoryGirl.create(:rsvp_slot, :instrument => bass, :music_session => session2)
@ -73,22 +85,176 @@ describe EmailBatchScheduledSessions do
# oo.rsvp_request_slot.update_attributes(chosen: true)
# oo = FactoryGirl.create(:rsvp_request, :user => vocalist, :rsvp_slot => oo)
# oo.rsvp_request_slot.update_attributes(chosen: true)
end
it 'sets up data properly' do
JamRuby::Score.createx(1, 'a', 1, 1, 'a', 1, 10)
JamRuby::Score.createx(1, 'a', 1, 2, 'a', 2, Score::MAX_YELLOW_LATENCY + 1)
expect(drummer.instruments.include?(drums)).to eq(true)
expect(drummer.instruments.include?(guitar)).to eq(true)
obj = scheduled_batch.fetch_recipients
expect(obj.count).to eq(2)
scheduled_batch.deliver_batch
expect(UserMailer.deliveries.length).to eq(2)
end
describe "everyone but loser has good enough scores" do
before(:each) do
JamRuby::Score.createx(1, 'a', 1, 1, 'a', 1, 10)
JamRuby::Score.createx(1, 'a', 1, 2, 'a', 2, (APP_CONFIG.max_yellow_full_score * 2) + 1)
@matching_instruments_users = [guitarist, bassist]
@good_score_users = [guitarist, bassist, vocalist] # not adding the drummer because he's the creator (he does have a good score to these others though)
end
describe "session1 is_unstructured" do
before(:each) do
session1.is_unstructured_rsvp = true
session1.save!
end
it 'finds anyone with good enough scores' do
obj = scheduled_batch.fetch_recipients
found_users = obj.map{ |user_and_sessions| user_and_sessions[0]}
@good_score_users.should =~ found_users
scheduled_batch.deliver_batch
expect(UserMailer.deliveries.length).to eq(@good_score_users.length)
end
end
it 'finds anyone with good enough scores and matching instruments' do
obj = scheduled_batch.fetch_recipients
#Database.dump("SELECT * FROM tmp_candidate_recipients") do |field, value|
# if field == 'receiver_id'
# User.find(value).first_name
# else
# value.to_s
# end
#end
found_users = obj.map{ |user_and_sessions| user_and_sessions[0]}
@matching_instruments_users.should =~ found_users
scheduled_batch.deliver_batch
expect(UserMailer.deliveries.length).to eq(@matching_instruments_users.length)
end
describe "closes a RSVP slot in session1" do
before(:each) do
request = FactoryGirl.create(:rsvp_request, user: loser)
FactoryGirl.create(:rsvp_request_rsvp_slot, chosen: true, rsvp_request: request, rsvp_slot: @bass_rsvp_slot)
end
it "finds one less" do
# the bass slot is closed; so we should not longer find the bassist in the results
@matching_instruments_users.delete(bassist).should_not be_nil
obj = scheduled_batch.fetch_recipients
found_users = obj.map{ |user_and_sessions| user_and_sessions[0]}
@matching_instruments_users.should =~ found_users
scheduled_batch.deliver_batch
expect(UserMailer.deliveries.length).to eq(@matching_instruments_users.length)
end
end
describe "2 more open sessions" do
before(:each) do
JamRuby::Score.createx(1, 'a', 1, session3_creator.last_jam_locidispid, 'a', 1, 5)
JamRuby::Score.createx(1, 'a', 1, session4_creator.last_jam_locidispid, 'a', 2, 20)
session3.touch
session4.touch
end
it "sessions are sorted by latency" do
obj = scheduled_batch.fetch_recipients
obj.length.should == @matching_instruments_users.length + [drummer, vocalist].length
# verify that all 4 users have the right sessions, in the right order
@matching_instruments_users.each do |user|
user_sessions = find_user_sessions(obj, user)
user_sessions.should == [session3, session1, session4]
end
user_sessions = find_user_sessions(obj, drummer)
user_sessions.should == [session3, session4]
user_sessions = find_user_sessions(obj, vocalist)
user_sessions.should == [session3, session4]
end
end
describe "21 more sessions" do
before(:each) do
20.times do |i|
creator = FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1)
FactoryGirl.create(:music_session,
:creator => creator,
:open_rsvps => true,
:is_unstructured_rsvp => true,
:scheduled_start => Time.now() + 2.days,
:created_at => Time.now - 1.hour)
end
end
it "prevents more than 20 sessions per user" do
obj = scheduled_batch.fetch_recipients
obj.length.should == 20 + 4 # 4 is vocalist, drummer, guitarist, and bassist. 20 is the users made in the before block
# the bassist should *potentially* the drummer's session, and these new 20 sessions. But we limit to 20.
user_sessions = find_user_sessions(obj, bassist)
user_sessions.length.should == 20
end
end
it 'finds anyone with good enough scores and matching instruments' do
obj = scheduled_batch.fetch_recipients
#Database.dump("SELECT * FROM tmp_candidate_recipients") do |field, value|
# if field == 'receiver_id'
# User.find(value).first_name
# else
# value.to_s
# end
#end
found_users = obj.map{ |user_and_sessions| user_and_sessions[0]}
@matching_instruments_users.should =~ found_users
scheduled_batch.deliver_batch
expect(UserMailer.deliveries.length).to eq(@matching_instruments_users.length)
end
describe "open_rsvp=false" do
before(:each) do
session1.open_rsvps = false
session1.save!
session2.open_rsvps = false
session2.save!
end
it "won't find an open_rsvps=false session without an invitation" do
obj = scheduled_batch.fetch_recipients
expect(obj.count).to eq(0)
end
describe "with invitations" do
let(:invitation) {friend(drummer, guitarist); FactoryGirl.create(:invitation, sender: drummer, receiver:guitarist, music_session: session1)}
before(:each) do
invitation.touch
end
it "finds user with the invite" do
obj = scheduled_batch.fetch_recipients
found_users = obj.map{ |user_and_sessions| user_and_sessions[0]}
[guitarist].should =~ found_users
scheduled_batch.deliver_batch
expect(UserMailer.deliveries.length).to eq(1)
end
end
end
end
it 'handles large batches' do
creators = []
8.downto(1) do |nn|

View File

@ -0,0 +1,61 @@
require 'spec_helper'
describe GenericState do
def database_env (env)
singleton = GenericState.singleton
singleton.env = env
singleton.save!
end
def rails_env (env)
JamRuby::Environment.should_receive(:mode).any_number_of_times.and_return(env)
end
describe "allow_emails?" do
it "allows emails if database is production and env is production, with build number" do
database_env('production')
rails_env('production')
stub_const("ENV", {'BUILD_NUMBER' => 1})
GenericState.allow_emails?.should be_true
end
it "no emails if database is production and env is production, no build number" do
database_env('production')
rails_env('production')
stub_const("ENV", {'BUILD_NUMBER' => nil})
GenericState.allow_emails?.should be_false
end
it "allows emails if database is development and env is development" do
database_env('development')
rails_env('development')
GenericState.allow_emails?.should be_true
end
it "no emails if database development, and environment is test" do
database_env('development')
rails_env('test')
GenericState.allow_emails?.should be_false
end
it "no emails if database production, and environment is development" do
database_env('production')
rails_env('development')
stub_const("ENV", {'BUILD_NUMBER' => 1})
GenericState.allow_emails?.should be_false
end
it "no emails if database production, and environment is test" do
database_env('production')
rails_env('development')
GenericState.allow_emails?.should be_false
end
end
end

View File

@ -421,7 +421,7 @@ describe MusicSession do
let(:network_score) { 20 }
before(:each) do
Score.createx(conn.locidispid, conn.client_id, conn.addr, searcher_conn.locidispid, searcher_conn.client_id, searcher_conn.addr, network_score, nil)
Score.createx(conn.locidispid, conn.client_id, conn.addr, searcher_conn.locidispid, searcher_conn.client_id, searcher_conn.addr, network_score, nil, nil, {auserid: creator.id, buserid: searcher.id})
end
it "no results" do
@ -499,7 +499,7 @@ describe MusicSession do
FactoryGirl.create(:invitation, receiver:invitee, sender:creator, music_session: music_session)
# create a score between invitee, and searcher
Score.createx(invitee.last_jam_locidispid, 'immaterial', 1, searcher_conn.locidispid, searcher_conn.client_id, searcher_conn.addr, network_score, nil)
Score.createx(invitee.last_jam_locidispid, 'immaterial', 1, searcher_conn.locidispid, searcher_conn.client_id, searcher_conn.addr, network_score, nil, nil, {auserid: invitee.id, buserid: searcher.id})
music_sessions, user_scores = sms(searcher, default_opts)
music_sessions.length.should == 1
@ -580,14 +580,16 @@ describe MusicSession do
end
it "searcher_1" do
# create a bad score between searcher_1 and creator_1 (but we should still see it sort 1st because it's got an RSVP to the searcher)
Score.createx(searcher_conn_1.locidispid, searcher_conn_1.client_id, searcher_conn_1.addr, creator_conn_1.locidispid, creator_conn_1.client_id, creator_conn_1.addr, bad_network_score, nil)
Score.createx(searcher_conn_1.locidispid, searcher_conn_1.client_id, searcher_conn_1.addr, creator_conn_1.locidispid, creator_conn_1.client_id, creator_conn_1.addr, bad_network_score, nil, nil, {auserid: searcher_1.id, buserid: creator_1.id})
# create a fair score between searcher_1 and creator_2 (but we should still see it sort 2st because it's got an invitation to the searcher)
Score.createx(searcher_conn_1.locidispid, searcher_conn_1.client_id, searcher_conn_1.addr, creator_conn_2.locidispid, creator_conn_2.client_id, creator_conn_2.addr, fair_network_score, nil)
Score.createx(searcher_conn_1.locidispid, searcher_conn_1.client_id, searcher_conn_1.addr, creator_conn_2.locidispid, creator_conn_2.client_id, creator_conn_2.addr, fair_network_score, nil, nil, {auserid: searcher_1.id, buserid: creator_2.id})
# create a good score between searcher_1 and creator_3 (but we should still see it sort last because it's an open session; no affiliation with the searcher)
Score.createx(searcher_conn_1.locidispid, searcher_conn_1.client_id, searcher_conn_1.addr, creator_conn_3.locidispid, creator_conn_3.client_id, creator_conn_3.addr, good_network_score, nil)
Score.createx(searcher_conn_1.locidispid, searcher_conn_1.client_id, searcher_conn_1.addr, creator_conn_3.locidispid, creator_conn_3.client_id, creator_conn_3.addr, good_network_score, nil, nil, {auserid: searcher_1.id, buserid: creator_3.id})
music_sessions, user_scores = sms(searcher_1, {client_id: searcher_conn_1.client_id})
@ -777,5 +779,20 @@ describe MusicSession do
end
end
end
describe "scheduled session rescheduled logic" do
it "detect change to scheduling info" do
music_session1.description = "Hey!"
music_session1.save!
music_session1.scheduling_info_changed.should be_false
music_session1.scheduled_start = Time.now - 1.days
music_session1.save!
music_session1.scheduling_info_changed.should be_true
end
end
end

View File

@ -38,6 +38,7 @@ describe ScoreHistory do
score_history.from_user_id.should == score1.auserid
score_history.from_latency_tester_id.should == score1.alatencytestid
score_history.from_addr.should == score1.aaddr
score_history.from_locidispid.should == score1.alocidispid
score_history.from_isp.should == austin[:jamisp].jam_company.company
score_history.from_country.should == austin[:geoiplocation].countrycode
score_history.from_region.should == austin[:geoiplocation].region
@ -49,6 +50,7 @@ describe ScoreHistory do
score_history.to_user_id.should == score1.buserid
score_history.to_latency_tester_id.should == score1.blatencytestid
score_history.to_addr.should == score1.baddr
score_history.to_locidispid.should == score1.blocidispid
score_history.to_isp.should == dallas[:jamisp].jam_company.company
score_history.to_country.should == dallas[:geoiplocation].countrycode
score_history.to_region.should == dallas[:geoiplocation].region

View File

@ -18,8 +18,8 @@ describe "MusicSessionScheduler" do
MusicSession.all.count.should == 1
# end the session
ms.session_removed_at = Time.now
# move the session back more than 4 hours
ms.scheduled_start = Time.now - 5.hours
ms.save!
# run the scheduler again
@ -27,7 +27,6 @@ describe "MusicSessionScheduler" do
MusicSession.all.count.should == 2
MusicSession.where(:session_removed_at => nil).count.should == 1
MusicSession.where(:next_session_scheduled => true).count.should == 1
end
@ -40,7 +39,7 @@ describe "MusicSessionScheduler" do
@scheduler.run
MusicSession.all.count.should == 1
ms.session_removed_at = Time.now
ms.scheduled_start = Time.now - 3.hours
ms.save!
@scheduler.run

View File

@ -63,6 +63,70 @@ describe "RenderMailers", :slow => true do
it { @filename="welcome_betauser"; InvitedUserMailer.welcome_betauser(admin_invited_user).deliver }
end
describe "Daily Scheduled Session emails" do
let (:scheduled_batch) { FactoryGirl.create(:email_batch_scheduled_session) }
let(:music_session) { FactoryGirl.create(:music_session) }
let (:drums) { FactoryGirl.create(:instrument, :description => 'drums') }
let (:guitar) { FactoryGirl.create(:instrument, :description => 'guitar') }
let (:bass) { FactoryGirl.create(:instrument, :description => 'bass') }
let (:vocals) { FactoryGirl.create(:instrument, :description => 'vocal') }
let (:drummer) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:guitarist) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:bassist) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:vocalist) { FactoryGirl.create(:user, :last_jam_locidispid => 1, :last_jam_addr => 1) }
let (:session1) do
FactoryGirl.create(:music_session,
:creator => drummer,
:scheduled_start => Time.now() + 2.days,
:musician_access => true,
:approval_required => false,
:created_at => Time.now - 1.hour)
end
let (:session2) do
FactoryGirl.create(:music_session,
:creator => drummer,
:scheduled_start => Time.now() + 2.days,
:musician_access => true,
:approval_required => false,
:created_at => Time.now - 1.hour)
end
before(:each) do
BatchMailer.deliveries.clear
scheduled_batch.reset!
drummer.musician_instruments << FactoryGirl.build(:musician_instrument, user: drummer, instrument: drums, proficiency_level: 2)
drummer.musician_instruments << FactoryGirl.build(:musician_instrument, user: drummer, instrument: guitar, proficiency_level: 2)
guitarist.musician_instruments << FactoryGirl.build(:musician_instrument, user: guitarist, instrument: guitar, proficiency_level: 2)
guitarist.musician_instruments << FactoryGirl.build(:musician_instrument, user: guitarist, instrument: bass, proficiency_level: 2)
vocalist.musician_instruments << FactoryGirl.build(:musician_instrument, user: vocalist, instrument: vocals, proficiency_level: 2)
FactoryGirl.create(:rsvp_slot, :instrument => drums, :music_session => session1)
FactoryGirl.create(:rsvp_slot, :instrument => guitar, :music_session => session1)
FactoryGirl.create(:rsvp_slot, :instrument => bass, :music_session => session1)
FactoryGirl.create(:rsvp_slot, :instrument => drums, :music_session => session2)
FactoryGirl.create(:rsvp_slot, :instrument => guitar, :music_session => session2)
FactoryGirl.create(:rsvp_slot, :instrument => bass, :music_session => session2)
JamRuby::Score.createx(1, 'a', 1, 1, 'a', 1, 10)
JamRuby::Score.createx(1, 'a', 1, 2, 'a', 2, Score::MAX_YELLOW_LATENCY + 1)
end
after(:each) do
BatchMailer.deliveries.length.should == 1
mail = BatchMailer.deliveries[0]
save_emails_to_disk(mail, @filename)
end
it "daily sessions" do @filename="daily_sessions"; scheduled_batch.deliver_batch end
end
end
def save_emails_to_disk(mail, filename)

View File

@ -109,6 +109,19 @@ def app_config
def max_track_part_upload_failures
3
end
def max_good_full_score
20
end
def max_yellow_full_score
35
end
def max_red_full_score
50
end
private
def audiomixer_workspace_path
@ -169,4 +182,9 @@ def fake_geo_124_zip(geoisp_csv)
end
zipfile
end
def friend(user1, user2)
FactoryGirl.create(:friendship, user: user1, friend: user2)
FactoryGirl.create(:friendship, user: user2, friend: user1)
end

View File

@ -4,7 +4,9 @@
context.JK = context.JK || {};
context.JK.AccountProfileScreen = function(app) {
var $document = $(document);
var logger = context.JK.logger;
var EVENTS = context.JK.EVENTS;
var api = context.JK.Rest();
var userId;
var user = {};
@ -63,6 +65,7 @@
$('select#user_birth_date_1i', content_root).val(parseInt(birthDateYear));
$('select#user_birth_date_2i', content_root).val(parseInt(birthDateMonth));
$('select#user_birth_date_3i', content_root).val(parseInt(birthDateDay));
}
// update instruments
@ -87,6 +90,7 @@
}
context.JK.dropdown($('select', content_root));
}
function isUserInstrument(instrument, userInstruments) {
@ -317,7 +321,9 @@
.always(function() { loadingCitiesData = false;})
}
}
})
context.JK.dropdown($('select'));
}
function navToAccount() {
@ -366,6 +372,9 @@
},
null,
true);
$document.triggerHandler(EVENTS.USER_UPDATED, response);
}
function postUpdateProfileFailure(xhr, textStatus, errorMessage) {

View File

@ -6,6 +6,7 @@
context.JK.AccountProfileAvatarScreen = function(app) {
var self = this;
var logger = context.JK.logger;
var EVENTS = context.JK.EVENTS;
var rest = context.JK.Rest();
var user = {};
var tmpUploadPath = null;
@ -381,9 +382,9 @@
self.userDetail = response;
// notify any listeners that the avatar changed
userDropdown.loadMe();
// userDropdown.loadMe();
// $('.avatar_large img').trigger('avatar_changed', [self.userDetail.photo_url]);
$(document).triggerHandler(EVENTS.USER_UPDATED, response);
app.notify(
{ title: "Avatar Changed",
text: "You have updated your avatar successfully."

View File

@ -28,11 +28,15 @@
$('.schedule-recurrence', $dialog).html("Recurs " + response.recurring_mode + " on this day at this time");
}
var hasOpenSlots = response.open_slots && response.open_slots.length > 0;
if (response['is_unstructured_rsvp?']) {
$('.rsvp-instruments', $dialog).append('<input type="checkbox" value="unstructured"/>Any Instrument<br/>');
var checkedState = hasOpenSlots ? '' : 'checked="checked"'
$('.rsvp-instruments', $dialog).append('<input type="checkbox" ' + checkedState + ' value="unstructured"/>Play Any Instrument You Like<br/>');
}
if (response.open_slots && response.open_slots.length > 0) {
if (hasOpenSlots) {
$.each(response.open_slots, function(index, val) {
var instrument = val.instrument_id;

View File

@ -201,13 +201,13 @@
}
function renderNotConnected() {
console.log("RENDER NOT CONNECTED!!!!!!!!!")
logger.debug("text-message dialog: render not connected")
$interactionBlocker.addClass('active');
$disconnectedMsg.addClass('active');
}
function renderConnected() {
console.log("RENDER CONNECTED!!!!!!!!!")
logger.debug("text-message dialog: render connected")
$interactionBlocker.removeClass('active');
$disconnectedMsg.removeClass('active');
}

View File

@ -203,6 +203,19 @@
return temp;
},
convertPercentToAudioTaper: function (input) {
// composite function resembling audio taper
if (input <= 1) { return -80; }
if (input <= 28) { return (2 * input - 80); }
if (input <= 79) { return (0.5 * input - 38); }
if (input < 99) { return (0.875 * input - 67.5); }
if (input >= 99) { return 20; }
},
setFaderValue: function (faderId, faderValue) {
var $fader = $('[fader-id="' + faderId + '"]');
this.setHandlePosition($fader, faderValue);

View File

@ -89,8 +89,8 @@
// these are raw scores as reported by client (round trip times)
if (score == null) return "purple";
if (0 < score && score <= 40) return "green";
if (40 < score && score <= 80) return "yellow";
if (80 < score && score <= 120) return "red";
if (40 < score && score <= 70) return "yellow";
if (70 < score && score <= 100) return "red";
return "blue";
}
@ -98,8 +98,8 @@
// these are raw scores as reported by client (round trip times)
if (score == null) return "missing";
if (0 < score && score <= 40) return "good";
if (40 < score && score <= 80) return "moderate";
if (80 < score && score <= 120) return "poor";
if (40 < score && score <= 70) return "moderate";
if (70 < score && score <= 100) return "poor";
return "unacceptable";
}
@ -126,6 +126,7 @@
var mVals, musician, renderings='';
var instr_logos, instr;
var follows, followVals, aFollow;
var myAudioLatency = musicianList.my_audio_latency;
for (ii=0, len=musicians.length; ii < len; ii++) {
musician = musicians[ii];
@ -165,7 +166,7 @@
};
var musician_actions = context.JK.fillTemplate(aTemplate, actionVals);
var joined_score = musician['joined_score']
var full_score = musician['full_score'];
mVals = {
avatar_url: context.JK.resolveAvatarUrl(musician.photo_url),
profile_url: "/client#/profile/" + musician.id,
@ -180,22 +181,27 @@
musician_id: musician['id'],
musician_follow_template: follows,
musician_action_template: musician_actions,
musician_one_way_score: score_to_text(joined_score),
musician_score_color: score_to_color(joined_score),
musician_score_color_alt: score_to_color_alt(joined_score)
musician_one_way_score: score_to_text(full_score),
musician_score_color: score_to_color(full_score),
musician_score_color_alt: score_to_color_alt(full_score)
};
var musician_row = context.JK.fillTemplate(mTemplate, mVals);
renderings += musician_row;
var $rendering = $(context.JK.fillTemplate(mTemplate, mVals))
var $offsetParent = $results.closest('.content');
var options = {positions: ['top', 'bottom', 'right', 'left'], offsetParent: $offsetParent};
var scoreOptions = {positions: ['right', 'top', 'bottom', 'left'], offsetParent: $offsetParent, width:'600px'};
context.JK.helpBubble($('.follower-count', $rendering), 'musician-follower-count', {}, options);
context.JK.helpBubble($('.friend-count', $rendering), 'musician-friend-count', {}, options);
context.JK.helpBubble($('.recording-count', $rendering), 'musician-recording-count', {}, options);
context.JK.helpBubble($('.session-count', $rendering), 'musician-session-count', {}, options);
context.JK.helpBubble($('.score-count', $rendering), 'musician-score-count',
{full_score: full_score ? Math.round(full_score / 2) : null, my_gear_latency: myAudioLatency, their_gear_latency:musician['audio_latency'], internet_latency: musician['score']},
scoreOptions)
$results.append($rendering);
}
var $renderings = $(renderings);
var $offsetParent = $results.closest('.content');
var options = {positions: ['top', 'bottom', 'right', 'left'], offsetParent: $offsetParent}
context.JK.helpBubble($('.follower-count', $renderings), 'musician-follower-count', {}, options);
context.JK.helpBubble($('.friend-count', $renderings), 'musician-friend-count', {}, options);
context.JK.helpBubble($('.recording-count', $renderings), 'musician-recording-count', {}, options);
context.JK.helpBubble($('.session-count', $renderings), 'musician-session-count', {}, options);
$results.append($renderings);
$('.search-m-friend').on('click', friendMusician);
$('.search-m-follow').on('click', followMusician);

View File

@ -273,11 +273,11 @@
}
function clearResults() {
$('table#sessions-active').empty();
$('table#sessions-active').find("tr:gt(0)").remove();
currentScheduledSessionsPage = 0;
$ssScroller.infinitescroll('resume');
$('table#sessions-scheduled').empty();
$('table#sessions-scheduled').find("tr:gt(0)").remove();
$ssNoMoreEntries.hide();
}

View File

@ -32,8 +32,9 @@
SHOW_SIGNUP : 'show_signup',
SHOW_SIGNIN : 'show_signin',
RSVP_SUBMITTED: 'rsvp_submitted',
RSVP_CANCELED : 'rsvp_canceled'
}
RSVP_CANCELED : 'rsvp_canceled',
USER_UPDATED : 'user_updated'
};
context.JK.ALERT_NAMES = {
NO_EVENT : 0,

View File

@ -883,11 +883,7 @@
dataType: "json",
contentType: 'application/json',
url: "/api/users/progression/certified_gear",
processData: false,
data: JSON.stringify({
success: options.success,
reason: options.reason
})
data: JSON.stringify(options)
});
}
@ -1184,17 +1180,6 @@
});
}
function updateAudioLatency(options) {
var id = getId(options);
return $.ajax({
type: "POST",
url: '/api/users/' + id + '/audio_latency',
dataType: "json",
contentType: 'application/json',
data: options,
});
}
function initialize() {
return self;
}
@ -1298,7 +1283,6 @@
this.getChatMessages = getChatMessages;
this.createDiagnostic = createDiagnostic;
this.getLatencyTester = getLatencyTester;
this.updateAudioLatency = updateAudioLatency;
return this;
};

View File

@ -218,9 +218,10 @@
hoveraction: val.session_id ? "session" : "",
avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
text: val.formatted_msg,
date: $.timeago(val.created_at)
date: $.timeago(val.created_at),
userId: val.source_user_id
});
$list.append(notificationHtml);
// val.description contains the notification record's description value from the DB (i.e., type)
@ -307,10 +308,15 @@
}
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text(actionText);
$action_btn.click(function() {
callback(payload);
});
if (actionText === '') {
$action_btn.hide();
}
else {
$action_btn.text(actionText);
$action_btn.click(function() {
callback(payload);
});
}
}
else if (type === context.JK.MessageType.MUSICIAN_RECORDING_SAVED || type === context.JK.MessageType.BAND_RECORDING_SAVED) {

View File

@ -117,14 +117,14 @@
var firstSession = function() {
var $firstSession = $scheduledSessions.children().first().find('input[name="scheduled-session-info"]');
$firstSession.attr('checked', 'checked');
createSessionSettings.selectedSessionId = $firstSession.attr('id');
createSessionSettings.selectedSessionId = $firstSession.attr('data-session-id');
};
if (createSessionSettings.selectedSessionId == null) {
firstSession();
}
else {
var $selectedSession = $scheduledSessions.children().first().find('input[name="scheduled-session-info"][id="' + createSessionSettings.selectedSessionId + '"]');
var $selectedSession = $scheduledSessions.children().first().find('input[name="scheduled-session-info"][data-session-id="' + createSessionSettings.selectedSessionId + '"]');
if ($selectedSession.length)
$selectedSession.attr('checked', 'checked');
else
@ -349,6 +349,7 @@
function beforeMoveStep1() {
if (createSessionSettings.createType == 'start-scheduled') {
createSessionSettings.selectedSessionId = $scheduledSessions.find('.iradio_minimal.checked input[name="scheduled-session-info"]').attr('data-session-id');
var session = scheduledSessions[createSessionSettings.selectedSessionId];
if(session == null) {
@ -382,7 +383,7 @@
var startTime = new Date(session.scheduled_start);
var diffTime = startTime.getTime() - currentTime.getTime();
if (diffTime > ONE_HOUR) {
var confirmDialog = new context.JK.ConfirmDialog(app, "Start Session Now",
var confirmDialog = new context.JK.ConfirmDialog(app, "START SESSION NOW",
"You are starting a session that is scheduled to begin more than one hour from now. Are you sure you want to do this?",
"Future Session", moveToFinish);
confirmDialog.initialize();
@ -416,7 +417,7 @@
createSessionSettings.startTime = $startTimeList.val();
createSessionSettings.endTime = $endTimeList.val();
createSessionSettings.notations = [];
createSessionSettings.selectedSessionId = $scheduledSessions.find('input[name="scheduled-session-info"][checked="checked"]').attr('id');
createSessionSettings.selectedSessionId = $scheduledSessions.find('.iradio_minimal.checked input[name="scheduled-session-info"]').attr('data-session-id');
createSessionSettings.timezone.value = $timezoneList.val();
createSessionSettings.timezone.label = $timezoneList.get(0).options[$timezoneList.get(0).selectedIndex].text;
createSessionSettings.recurring_mode.label = $recurringModeList.get(0).options[$recurringModeList.get(0).selectedIndex].text;
@ -569,22 +570,28 @@
}
function startSessionClicked() {
var $btn = $(this);
if($btn.is('.disabled')) { return false; }
$btn.addClass('disabled')
if(willOptionStartSession()) {
gearUtils.guardAgainstInvalidConfiguration(app)
.fail(function() {
$btn.removeClass('disabled')
app.notify(
{ title: "Unable to Start New Session",
text: "You can only start a session once you have working audio gear and a tested internet connection."
})
})
.done(function(){
startSession();
startSession($btn);
});
}
else {
startSession();
startSession($btn);
}
return false;
}
// did the user have to pick the RSVP slots explicitely?
@ -592,7 +599,7 @@
return createType == "immediately" || createType == "schedule-future" || createType == "rsvp";
}
function startSession() {
function startSession($startBtn) {
var data = {};
@ -682,36 +689,12 @@
}
var joinSession = function(sessionId) {
var tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
var options = {};
options.client_id = app.clientId;
options.session_id = sessionId;
options.as_musician = true;
options.tracks = tracks;
rest.joinSession(options)
.done(function(response) {
var invitationCount = data.invitations.length;
context.location = '/client#/session/' + sessionId;
context.JK.GA.trackSessionCount(data.musician_access, data.fan_access, invitationCount);
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
})
.fail(function(jqXHR) {
var handled = false;
if(jqXHR.status = 422) {
var response = JSON.parse(jqXHR.responseText);
if(response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track") {
app.notifyAlert("No Inputs Configured", $('<span>You will need to reconfigure your audio device.</span>'));
handled = true;
}
}
if(!handled) {
app.notifyServerError(jqXHR, "Unable to Create Session");
}
})
var invitationCount = data.invitations.length;
context.JK.GA.trackSessionCount(data.musician_access, data.fan_access, invitationCount);
// we redirect to the session screen, which handles the REST call to POST /participants.
logger.debug("joining session screen: " + sessionId)
context.location = '/client#/session/' + sessionId;
};
if (createSessionSettings.createType == 'start-scheduled') {
@ -734,9 +717,10 @@
}
})
.fail(function(jqXHR){
$startBtn.removeClass('disabled');
logger.debug("unable to schedule a session")
app.notifyServerError(jqXHR, "Unable to schedule a session");
});
})
}
}

View File

@ -315,6 +315,12 @@
null,
true);
}
else if(xhr.status == 422) {
var response = JSON.parse(jqXHR.responseText);
if(response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track") {
app.notifyAlert("No Inputs Configured", $('<span>You will need to reconfigure your audio device.</span>'));
}
}
else {
app.notifyServerError(xhr, 'Unable to Join Session');
}
@ -569,6 +575,7 @@
$voiceChat.show();
$voiceChat.attr('mixer-id', mixer.id);
var $voiceChatGain = $voiceChat.find('.voicechat-gain');
$voiceChatGain.attr('mixer-id', mixer.id);
var $voiceChatMute = $voiceChat.find('.voicechat-mute').attr('mixer-id', mixer.id);
var gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
@ -1147,8 +1154,8 @@
// volumes on trackVolumeObject, and call SetControlState to stick.
var sliderValue = percentToMixerValue(
currentMixerRangeMin, currentMixerRangeMax, volumePercent);
context.trackVolumeObject.volL = sliderValue;
context.trackVolumeObject.volR = sliderValue;
context.trackVolumeObject.volL = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent);
context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent);
// Special case for L2M mix:
if (mixerId === '__L2M__') {
logger.debug("L2M volumePercent=" + volumePercent);

View File

@ -21,9 +21,9 @@
var LATENCY = {
GOOD : {description: "GOOD", style: "latency-green", min: 0.0, max: 20.0},
MEDIUM : {description: "MEDIUM", style: "latency-yellow", min: 20.0, max: 40.0},
POOR : {description: "POOR", style: "latency-red", min: 40.0, max: 10000000000.0},
UNREACHABLE: {description: "UNREACHABLE", style: "latency-grey", min: -1, max: -1},
MEDIUM : {description: "MEDIUM", style: "latency-yellow", min: 20.0, max: 35.0},
POOR : {description: "POOR", style: "latency-red", min: 35.0, max: 50},
UNACCEPTABLE: {description: "UNACCEPTABLE", style: "latency-grey", min: 50, max: 10000000},
UNKNOWN: {description: "UNKNOWN", style: "latency-grey", min: -2, max: -2}
};
@ -350,7 +350,10 @@
}
function createLatency(user) {
var latencyStyle = LATENCY.UNREACHABLE.style, latencyDescription = LATENCY.UNREACHABLE.description
var latencyStyle;
var latencyDescription;
if (user.id === context.JK.currentUserId) {
latencyStyle = LATENCY.GOOD.style, latencyDescription = LATENCY.GOOD.description;
}
@ -358,24 +361,25 @@
else {
var latency = user.latency;
if (!latency || latency === 1000) {
// 1000 is a magical number returned by new scoring API to indicate one or more people in the session have an unknown score
if (!latency) {
latencyDescription = LATENCY.UNKNOWN.description;
latencyStyle = LATENCY.UNKNOWN.style;
}
else if (latency <= LATENCY.GOOD.max) {
latencyDescription = LATENCY.GOOD.description;
latencyStyle = LATENCY.GOOD.style;
}
else if (latency > LATENCY.MEDIUM.min && latency <= LATENCY.MEDIUM.max) {
latencyDescription = LATENCY.MEDIUM.description;
latencyStyle = LATENCY.MEDIUM.style;
}
else if (latency > LATENCY.POOR.min && latency <= LATENCY.UNACCEPTABLE.max) {
latencyDescription = LATENCY.POOR.description;
latencyStyle = LATENCY.POOR.style;
}
else {
if (latency <= LATENCY.GOOD.max) {
latencyDescription = LATENCY.GOOD.description;
latencyStyle = LATENCY.GOOD.style;
}
else if (latency > LATENCY.MEDIUM.min && latency <= LATENCY.MEDIUM.max) {
latencyDescription = LATENCY.MEDIUM.description;
latencyStyle = LATENCY.MEDIUM.style;
}
else {
latencyDescription = LATENCY.POOR.description;
latencyStyle = LATENCY.POOR.style;
}
latencyStyle = LATENCY.UNREACHABLE.style
latencyDescription = LATENCY.UNREACHABLE.description
}
}

View File

@ -87,17 +87,22 @@
var deferred = joinSessionRest(sessionId);
deferred
.done(function(){
.done(function(response){
logger.debug("calling jamClient.JoinSession");
// on temporary disconnect scenarios, a user may already be in a session when they enter this path
// so we avoid double counting
if(!alreadyInSession()) {
// on temporary disconnect scenarios, a user may already be in a session when they enter this path
// so we avoid double counting
if(response.music_session.participant_count == 1) {
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
}
else {
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);
}
}
recordingModel.reset();
client.JoinSession({ sessionID: sessionId });
refreshCurrentSession();
refreshCurrentSession(true);
server.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, trackChanges);
server.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, trackChanges);
server.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges);

View File

@ -6,7 +6,7 @@
context.JK = context.JK || {};
context.JK.UserDropdown = function (app) {
var EVENTS = context.JK.EVENTS;
var logger = context.JK.logger;
var rest = new JK.Rest();
var userMe = null;
@ -53,12 +53,16 @@
$('.shortcuts .test-network').on('click', function(e) {
app.layout.showDialog('network-test');
return false;
})
});
$('#header-avatar').on('avatar_changed', function (event, newAvatarUrl) {
updateAvatar(newAvatarUrl);
event.preventDefault();
return false;
});
$(document).on(EVENTS.USER_UPDATED, function(e, data) {
userMe = data;
updateHeader();
})
}
@ -115,5 +119,8 @@
loadMe();
}
this.loadMe = loadMe;
}
})(window, jQuery);
})(window, jQuery);

View File

@ -185,7 +185,7 @@
cornerRadius: 0,
cssStyles: {
fontSize: '11px',
color: 'white',
color: '#cccccc',
whiteSpace: 'normal'
}
};

View File

@ -174,7 +174,10 @@
$('.call-to-action').html('Tell the session organizer if you can no longer join this session');
$btnAction.html('CANCEL RSVP');
$btnAction.click(function(e) {
ui.launchRsvpCancelDialog(musicSessionId, rsvp.id);
ui.launchRsvpCancelDialog(musicSessionId, rsvp.id)
.one(EVENTS.RSVP_CANCELED, function() {
location.reload();
});
});
}
}
@ -184,7 +187,10 @@
$('.call-to-action').html("Tell the session organizer you'd like to play in this session");
$btnAction.html('RSVP NOW!');
$btnAction.click(function(e) {
ui.launchRsvpSubmitDialog(musicSessionId);
ui.launchRsvpSubmitDialog(musicSessionId)
.one(EVENTS.RSVP_SUBMITTED, function() {
location.reload();
})
});
}
})
@ -201,14 +207,6 @@
});
addLatencyDetails();
$(document).on(EVENTS.RSVP_SUBMITTED, function() {
location.reload();
});
$(document).on(EVENTS.RSVP_CANCELED, function() {
location.reload();
});
}
this.initialize = initialize;

View File

@ -433,7 +433,7 @@
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.pass, latencyScore);
rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore()});
rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore().latency});
}
function onGearTestFail(e, data) {

View File

@ -269,14 +269,6 @@
return result;
}
gearUtils.updateAudioLatency = function(app) {
var latency = jamClient.FTUEGetExpectedLatency().latency;
return rest.updateAudioLatency({client_id: app.clientId, audio_latency: latency})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, "Unable to sync audio latency")
});
}
// if the user has a good user network score, immediately returns with a resolved deferred object.
// if not, the user will have the network test dialog prompted... once it's closed, then you'll be told reject() if score is still bad, or resolve() if now good
gearUtils.guardAgainstBadNetworkScore = function(app) {

View File

@ -53,6 +53,7 @@
*= require ./searchResults
*= require ./clientUpdate
*= require ./musician
*= require ./help
*= require ./jquery-ui-overrides
*= require web/audioWidgets
*= require web/recordings

View File

@ -0,0 +1,43 @@
.screen {
.bt-wrapper {
.bt-content {
color:#cccccc;
font-size:14px;
p {
font-size:14px;
}
ul {
font-size:14px;
}
li {
margin-left:1em;
margin-bottom:.5em;
}
.definition {
font-weight:bold;
}
.help-musician-score-count {
.measurement {
}
.measurement-value {
font-size:24px;
}
.measurement-absent {
font-style:italic;
font-size:12px;
display:block;
}
}
}
}
}

View File

@ -312,5 +312,14 @@
.more-text-available {
}
}
.wrapword{
white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
white-space: pre-wrap; /* css-3 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
word-break: break-all;
white-space: normal;
}
}

View File

@ -599,9 +599,6 @@ strong {
}
}
body.jam.web.welcome .no-websocket-connection {
display:none;
}
body.jam.web.register .no-websocket-connection {
body.jam.web .no-websocket-connection {
display:none;
}

View File

@ -224,7 +224,8 @@ class ApiMusicSessionsController < ApiController
response.status = :unprocessable_entity
respond_with @music_session
else
Notification.send_scheduled_session_rescheduled(@music_session)
Notification.send_scheduled_session_rescheduled(@music_session) if @music_session.scheduling_info_changed
respond_with @music_session, responder: ApiResponder, :location => api_session_history_detail_url(@music_session)
end
else
@ -287,7 +288,8 @@ class ApiMusicSessionsController < ApiController
params[:id],
params[:client_id],
params[:as_musician],
params[:tracks]
params[:tracks],
params[:audio_latency]
)
if @connection.errors.any?

View File

@ -24,6 +24,9 @@ class ApiScoringController < ApiController
conn = Connection.where(client_id: clientid, user_id: current_user.id).first
if conn.nil? then render :json => {message: 'session not found'}, :status => 404; return end
if conn.locidispid.nil? then render :json => {message: 'no locidispid for connection'}, :status => 404; return end
# if !current_user.id.eql?(conn.user.id) then render :json => {message: 'session not owned by user'}, :status => 403; return end
result_client_ids = JamRuby::GetWork.get_work_list(conn.locidispid, conn.addr)

View File

@ -534,7 +534,7 @@ class ApiUsersController < ApiController
if !@user.errors.any?
# update audio gear latency information
@user.update_audio_latency(connection, params[:audio_latency])
@user.update_audio_latency(connection, params[:audio_latency]) if params[:audio_latency]
end
else
@user.failed_qualification(params[:reason])

View File

@ -197,11 +197,11 @@ class UsersController < ApplicationController
@slides = [
Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"),
Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/VexH4834o9I?autoplay=1"),
Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"),
Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"),
Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1"),
Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"),
Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/VexH4834o9I?autoplay=1"),
Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1")
Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"),
Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1")
]
@promo_buzz = PromoBuzz.active
@ -228,9 +228,6 @@ class UsersController < ApplicationController
if !@user.nil? && !@user.errors.any?
UserMailer.welcome_message(@user).deliver
sign_in @user
redirect_to :client
return
elsif !@user.nil?
# new user with validation errors;
logger.debug("#{@user} has errors. can not sign in until remedied. #{@user.errors.inspect}")
@ -463,6 +460,9 @@ JS
end
def load_location(remote_ip, location = nil)
# useful if you need to repro something on 127.0.0.1
# remote_ip = ' 23.119.29.89'
@location = location
if @location.nil?
@ -471,7 +471,6 @@ JS
@location[:country] = "US" if @location[:country].nil?
# right now we only accept US signups for beta
@countriesx = MaxMindManager.countries
# populate regions based on current country
@regions = MaxMindManager.regions(@location[:country])

View File

@ -60,7 +60,7 @@ glue :music_session do
attributes :id, :name, :location, :photo_url
}
child(:active_music_session => :music_session) do
child(:active_music_session => :active_music_session) do
# only show mount info if fan_access is public. Eventually we'll also need to show this in other scenarios, like if invited
child({:mount => :mount}, :if => lambda { |music_session| music_session.fan_access}) {
attributes :id, :name, :sourced, :listeners, :bitrate, :subtype, :url

View File

@ -0,0 +1,13 @@
object @connection
extends "api_music_sessions/participant_show"
attributes :music_session_id
child(:music_session => :music_session) {
attributes :id
node :participant_count do |music_session|
music_session.connections.length
end
}

View File

@ -1,11 +1,8 @@
object @connection
attributes :ip_address, :client_id
attributes :ip_address, :client_id, :user_id
attribute :aasm_state => :connection_state
node(:user_id, :if => lambda { |connection| connection.user.friends?(current_user) }) do |connection|
connection.user_id
end
child(:tracks => :tracks) {
attributes :id, :instrument_id, :sound
}

View File

@ -41,9 +41,13 @@ if @search.musicians_filter_search?
node :page_count do |foo|
@search.page_count
end
node :my_audio_latency do |user|
current_user.last_jam_audio_latency
end
child(:results => :musicians) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url, :biography, :joined_score, :regionname
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score, :audio_latency
node :is_friend do |musician|
@search.is_friend?(musician)

View File

@ -79,7 +79,7 @@
<div class="field right w45 birth_date">
<label>Birth Date:</label>
<%= date_select("user", "birth_date", :use_short_month => true, :start_year => 1900, :end_year => Time.now.year - 18, :order => [:month, :day, :year], :default => -25.years.from_now) %>
<%= date_select("user", "birth_date", :use_short_month => true, :start_year => 1900, :end_year => Time.now.year - 18, :order => [:month, :day, :year], :default => -25.years.from_now, :html=>{:class => "account-profile-birthdate"} ) %>
</div>
<br class="clearall"/>

View File

@ -89,7 +89,7 @@
%td
{{data.latency}}
.right
%a{href: "/client#/profile/{{data.user_id}}", class: 'button-orange left', 'user-id' => "{{data.user_id}}", target: "_blank"} PROFILE
%a{href: "/client#/profile/{{data.user_id}}", class: 'button-orange left', 'user-id' => "{{data.user_id}}"} PROFILE
%a{href: "#", class: 'button-orange left approveRsvpRequest', 'user-id' => "{{data.user_id}}", 'request-id' => "{{data.request_id}}"} APPROVE
%a{href: "#", class: 'button-orange left declineRsvpRequest', 'user-id' => "{{data.user_id}}", 'request-id' => "{{data.request_id}}"} DECLINE
.clearall

View File

@ -80,7 +80,6 @@
Fan Access:
.right-column
%select#session-prop-fans-access
%option{value: "listen-chat-band"} Fans may listen, chat with the band
%option{value: "listen-chat-each", selected: "selected"} Fans may listen, chat with each other
%option{value: "no-listen-chat"} Fans may not listen to session
.clearall.right-no-left
@ -95,7 +94,6 @@
%option{value: "Standard"} Standard
%option{value: "Creative Commons"} Creative Commons
%option{value: "Offline"} Offline
%option{value: "Jamtracks"} Jamtracks
.clearall.left-column
Notation Files:
.right-column

View File

@ -56,4 +56,26 @@
<script type="text/template" id="template-help-musician-session-count">
The number of sessions that this user has played in.
</script>
<script type="text/template" id="template-help-musician-score-count">
<div class="help-musician-score-count">
<p>The score shown is the one-way latency (or delay) in milliseconds from you to this user. This score is calculated using the following three values that JamKazam gathers:</p>
<ul>
<li><span class="definition">Your Audio Gear Latency:</span> <span class="measurement my-gear-latency"><span class="measurement-value">{{data.my_gear_latency ? data.my_gear_latency + ' ms': '13 ms*'}}</span> <span class="measurement-absent">{{data.my_gear_latency ? '' : "(you have not qualified any gear, so we picked an average gear latency)"}}</span></span></li>
<li><span class="definition">Their Audio Gear Latency:</span> <span class="measurement their-gear-latency"><span class="measurement-value">{{data.their_gear_latency ? data.their_gear_latency + ' ms': '13 ms*'}}</span> <span class="measurement-absent">{{data.their_gear_latency ? '' : "(they have not qualified any gear, so we picked an average gear latency)"}}</span></span></li>
<li><span class="definition">Round-trip Internet Latency:</span> <span class="measurement internet-latency"><span class="measurement-value">{{data.internet_latency ? data.internet_latency + ' ms': '?'}}</span> <span class="measurement-absent">{{data.internet_latency ? '' : "(we have not scored you with this user yet)"}}</span></span></li>
</ul>
<p> <span class="definition">Total One-Way Latency:</span> <span class="measurement my-gear-latency"><span class="measurement-value">( {{data.my_gear_latency ? data.my_gear_latency: '13'}} + {{data.their_gear_latency ? data.their_gear_latency: '13'}} + {{data.internet_latency ? data.internet_latency: '?'}} ) / 2 = {{data.full_score ? data.full_score + ' ms' : "?"}}</span> <span class="measurement-absent">{{data.full_score ? '' : "(when we don't know internet latency, we don't try to guess your one-way latency)"}}</span></span>
<p>We categorize this score as good, fair, poor, unacceptable, or unknown. Those categories are defined as follows:
<ul>
<li><span class="definition">Good:</span> 20ms or less <img src="/assets/content/icon_green_score.png" /></li>
<li><span class="definition">Fair:</span> 20ms to 35ms <img src="/assets/content/icon_yellow_score.png" /></li>
<li><span class="definition">Poor:</span> 35ms to 50ms <img src="/assets/content/icon_red_score.png" /></li>
<li><span class="definition">Unacceptable:</span> Above 50ms <img src="/assets/content/icon_blue_score.png" /></li>
<li><span class="definition">Unknown:</span> No internet score is available between you and them. <img src="/assets/content/icon_purple_score.png" /></li>
</ul>
</div>
</script>

View File

@ -23,7 +23,7 @@
<% end -%>
<!-- Session Row Template -->
<script type="text/template" id="template-find-musician-row">
<script type="text/template" id="template-find-musician-row">
<div class="profile-band-list-result musician-list-result">
<div class="f11" data-hint="container">
<div class="left" style="width:63px;margin-top:-12px;">

View File

@ -456,7 +456,7 @@
<!-- Scheduled session template -->
<script type="text/template" id="template-scheduled-session">
<li>
<input type="radio" name="scheduled-session-info" id="{{data.id}}" value="false" />
<input type="radio" name="scheduled-session-info" data-session-id="{{data.id}}" value="false" />
<label for="{{data.id}}" class="radio-text w85">
{{data.scheduled_start}} : {{data.name}}
</label>

View File

@ -1,7 +1,7 @@
<h2><%= title %></h2>
<br/>
<div class="findsession-container">
<table class="findsession-table" cellspacing="0" cellpadding="0" border="0">
<table id="<%= category %>" class="findsession-table" cellspacing="0" cellpadding="0" border="0">
<!-- header -->
<tr>
<th align="left" width="30%">SESSION</th>
@ -12,7 +12,4 @@
</tr>
<!-- session row goes here -->
</table>
<table id="<%= category %>" class="findsession-table" cellspacing="0" cellpadding="0" border="0">
<!-- session row goes here -->
</table>
</div>

View File

@ -105,7 +105,7 @@
<span>Chat is available when in session.</span>
</div>
<div class="chat-list-scroller">
<div class="previous-chat-list">
<div class="previous-chat-list wrapword">
</div>
</div>
<div class="chat-sender">
@ -208,8 +208,8 @@
<!-- Notification panel template -->
<script type="text/template" id="template-notification-panel">
<li session-id="{sessionId}" notification-id="{notificationId}">
<div class="avatar-small"><img src="{avatar_url}" /></div>
<div session-id="{sessionId}" hoveraction="session" class="note-text">
<div class="avatar-small" user-id="{userId}" hoveraction="musician"><img src="{avatar_url}" /></div>
<div session-id="{sessionId}" hoveraction="hoveraction" class="note-text">
{text}<br/>
<em>{date}</em>
<div class="note-delete">

View File

@ -170,11 +170,11 @@
%h2 Tutorial Videos
%ul
%li
%a{href: '#'} Creating a Session
%a{href: 'https://www.youtube.com/watch?v=EZZuGcDUoWk', rel: 'external'} Creating a Session
%li
%a{href: '#'} Finding a Session
%a{href: 'https://www.youtube.com/watch?v=xWponSJo-GU', rel: 'external'} Finding a Session
%li
%a{href: '#'} Playing in a Session
%a{href: 'https://www.youtube.com/watch?v=zJ68hA8-fLA', rel: 'external'} Playing in a Session
%li
%a{href: '#'} Connecting with Other Musicians
%li
@ -184,9 +184,9 @@
%h2 Other Valuable Resource Links
%ul
%li
%a{href: '#'} JamKazam Support Center
%a{href: 'https://jamkazam.desk.com/', rel: 'external'} JamKazam Support Center
%li
%a{href: '#'} JamKazam Community Forum
%a{href: 'http://forums.jamkazam.com', rel: 'external'} JamKazam Community Forum
.wizard-buttons
%script{type: 'text/template', id: 'template-ftuesteps'}

View File

@ -12,7 +12,7 @@
.error{:style => 'display:none'} You must select at least 1 instrument.
.rsvp-instruments
.comment-instructions Enter a message to the other musicians in the session (optional):
.comment-instructions Enter a message to the other musicians in the session (optional):
%textarea.txtComment{rows: '2', placeholder: 'Enter a comment...'}
.buttons
.left

View File

@ -96,7 +96,10 @@
%br/
.left.w65.ib.still-needed
%strong Still Needed
- if @open_slots.blank?
- if @music_session.is_unstructured_rsvp
.clearall.left.w100.h20.ib.mb10
Open to any instrument
- elsif @open_slots.blank?
.clearall.left.w100.h20.ib.mb10
All slots are taken
- else
@ -108,8 +111,6 @@
= slot.instrument_id.capitalize
= "(#{slot.proficiency_desc})"
%br{:clear => "all"}/
%br/
.w65.ib.invited
%strong Invited

View File

@ -55,7 +55,7 @@
<option value="" <%= @location[:state].blank? ? "selected" : "" %>>State/Province</option>
<% @regions.each do |region| %>
<% unless region.blank? %>
<option value="<%= region[:region] %>" <%= @location[:state] == region ? "selected" : "" %>><%= region[:name] %></option>
<option value="<%= region[:region] %>" <%= @location[:state] == region[:region] ? "selected" : "" %>><%= region[:name] %></option>
<% end %>
<% end %>

View File

@ -1,8 +1,7 @@
<% provide(:title, 'Signup Confirmation') %>
<% if @user.nil? %>
<h1>Signup Already Confirmed</h1>
<h1>Signup Already Confirmed</h1>
<br/>
<br/>
<p>
@ -13,5 +12,14 @@
Please proceed to <%= link_to 'the home page', root_path %> and log in.
</p>
<% else %>
The server had a problem. Please try to confirm your email later.
<h1>Email Confirmed</h1>
<br/>
<br/>
<p>
You have successfully confirmed this email address.
</p>
<br/>
<p>
You can proceed to the website <%= link_to 'home page', root_path %> and sign in, or close this page in your browser.
</p>
<% end %>

View File

@ -249,5 +249,10 @@ if defined?(Bundler)
# recording upload/download configs
config.max_track_upload_failures = 10
config.max_track_part_upload_failures = 3
# scoring thresholds for 'full score', which is 1/2 your gear + 1/2 their gear + and 1/2 ping time
config.max_good_full_score = 20
config.max_yellow_full_score = 35
config.max_red_full_score = 50
end
end

View File

@ -1,5 +1,7 @@
@@log = Logging.logger['EmailInitializer']
ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.delivery_method = Rails.env == "test" ? :test : :smtp
ActionMailer::Base.delivery_method = GenericState.allow_emails? ? :smtp : :test
ActionMailer::Base.smtp_settings = {
:address => Rails.application.config.email_smtp_address,
:port => Rails.application.config.email_smtp_port,
@ -8,4 +10,6 @@ ActionMailer::Base.smtp_settings = {
:user_name => Rails.application.config.email_smtp_user_name,
:password => Rails.application.config.email_smtp_password ,
:enable_starttls_auto => Rails.application.config.email_smtp_starttls_auto
}
}
@@log.debug("ActionMailer.delivery_method = #{ActionMailer::Base.delivery_method}")

View File

@ -1,7 +1,7 @@
Resque.redis = Rails.application.config.redis_host
if !$rails_rake_task && Rails.env == 'development' && ENV['RUN_JOBS_INLINE'] == '1'
if !$rails_rake_task && Rails.env == 'development' && (ENV['RUN_JOBS_INLINE'] == '1' || ENV['RUN_INLINE_JOBS'] == '1')
Thread.new do
system('INTERVAL=1 bundle exec rake all_jobs')

View File

@ -16,7 +16,20 @@ task :scheduler => :environment do
# The schedule doesn't need to be stored in a YAML, it just needs to
# be a hash. YAML is usually the easiest.
Resque.schedule = YAML.load_file(File.join(File.dirname(__FILE__), '../..', 'config/scheduler.yml'))
config = YAML.load_file(File.join(File.dirname(__FILE__), '../..', 'config/scheduler.yml'))
if File.exist? File.join(File.dirname(__FILE__), '../..', 'config/scheduler_override.yml')
puts "scheduler_override file found. loading..."
override = YAML.load_file(File.join(File.dirname(__FILE__), '../..', 'config/scheduler_override.yml'))
if override # override will be false if file is empty
config.merge!(override)
else
puts "schedule_override is empty... skipping..."
end
end
Resque.schedule = config
# If your schedule already has +queue+ set for each job, you don't
# need to require your jobs. This can be an advantage since it's

View File

@ -57,7 +57,7 @@ describe ApiMusicSessionsController do
ams = FactoryGirl.create(:active_music_session, creator: other)
other_conn.join_the_session(ams.music_session, true, tracks, other, 10)
other_conn.errors.any?.should be_false
Score.createx(conn.locidispid, conn.client_id, conn.addr, other_conn.locidispid, other_conn.client_id, other_conn.addr, network_score, nil)
Score.createx(conn.locidispid, conn.client_id, conn.addr, other_conn.locidispid, other_conn.client_id, other_conn.addr, network_score, nil, nil, {auserid: user.id, buserid: other.id})
get :ams_index, {client_id: conn.client_id}
json = JSON.parse(response.body, :symbolize_names => true)
@ -75,7 +75,7 @@ describe ApiMusicSessionsController do
third_user.last_jam_audio_latency = 10 # RSVP's are an 'offline' search, meaning they use user.last_jam_audio_latency instead of connection.last_jam_audio_latency
third_user.last_jam_locidispid = conn.locidispid
third_user.save!
Score.createx(conn.locidispid, conn.client_id, conn.addr, other_conn.locidispid, other_conn.client_id, other_conn.addr, network_score, nil)
Score.createx(conn.locidispid, conn.client_id, conn.addr, other_conn.locidispid, other_conn.client_id, other_conn.addr, network_score, nil, nil, {auserid: user.id, buserid: other.id})
# set up a second RSVP (other than the creators, pointing to the third_user)
rsvp_slot = FactoryGirl.create(:rsvp_slot, music_session: ams.music_session, instrument: Instrument.find('piano'))
@ -139,14 +139,14 @@ describe ApiMusicSessionsController do
it "scores with invitees and RSVP's" do
# create a session with someone else in it, but no score
sms = FactoryGirl.create(:music_session, creator: other)
Score.createx(conn.locidispid, conn.client_id, conn.addr, other_conn.locidispid, other_conn.client_id, other_conn.addr, network_score, nil)
Score.createx(conn.locidispid, conn.client_id, conn.addr, other_conn.locidispid, other_conn.client_id, other_conn.addr, network_score, nil, nil, {auserid: user.id, buserid: other.id})
invitee = FactoryGirl.create(:user, last_jam_audio_latency: 30, last_jam_locidispid: 3)
FactoryGirl.create(:friendship, user: other, friend: invitee)
FactoryGirl.create(:friendship, user: invitee, friend: other)
FactoryGirl.create(:invitation, sender:other, receiver:invitee, music_session: sms)
Score.createx(invitee.last_jam_locidispid, 'immaterial', 1, conn.locidispid, conn.client_id, conn.addr, network_score, nil)
Score.createx(invitee.last_jam_locidispid, 'immaterial', 1, conn.locidispid, conn.client_id, conn.addr, network_score, nil, nil, {auserid: invitee.id, buserid: user.id})
get :sms_index, {client_id: conn.client_id}
json = JSON.parse(response.body, :symbolize_names => true)

View File

@ -2,6 +2,10 @@ include ActionDispatch::TestProcess # added for artifact_update http://stackover
FactoryGirl.define do
factory :user, :class => JamRuby::User do
ignore do
specific_instruments nil
end
sequence(:email) { |n| "person_#{n}@example.com"}
sequence(:first_name) { |n| "Person" }
sequence(:last_name) { |n| "#{n}" }
@ -33,8 +37,15 @@ FactoryGirl.define do
end
end
before(:create) do |user|
user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user)
before(:create) do |user, evaluator|
if evaluator.specific_instruments
evaluator.specific_instruments.each do |instrument|
puts "burp: "
user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user, instrument: instrument)
end
else
user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user)
end
end
factory :user_two_instruments do

View File

@ -11,7 +11,7 @@ describe "Account", :js => true, :type => :feature, :capybara_feature => true do
emulate_client
sign_in_poltergeist user
visit "/client#/account"
find('div.account-mid.identity')
end
@ -99,7 +99,25 @@ describe "Account", :js => true, :type => :feature, :capybara_feature => true do
user.subscribe_email.should be_false
user.first_name.should == "Bobby"
user.last_name.should == "Toes"
should have_selector('#profile #user', text: 'Bobby Toes')
# Update birth date and check updated birth date
jk_select("Jan", '#account-edit-profile-form #user_birth_date_2i')
jk_select("12", '#account-edit-profile-form #user_birth_date_3i')
jk_select("1960", '#account-edit-profile-form #user_birth_date_1i')
find("#account-edit-profile-submit").trigger(:click)
user.reload
user.birth_date == "1960-01-12"
should have_selector('#account-edit-profile-form .birth_date li.active', text: "Jan")
should have_selector('#account-edit-profile-form .birth_date li.active', text: "12")
should have_selector('#account-edit-profile-form .birth_date li.active', text: "1960")
}
end
describe "unsuccessfully" do

View File

@ -18,6 +18,9 @@ describe "Avatar", :js => true, :type => :feature, :capybara_feature => true do
visit "/client#/account/profile/avatar"
find('#account-edit-avatar-upload')
# within_frame 'filepicker_dialog' do
# attach_file '#fileUploadInput', Rails.root.join('spec', 'files', 'avatar.jpg')
# end
end
it {

View File

@ -17,6 +17,31 @@ describe "Create Session UI", :js => true, :type => :feature, :capybara_feature
end
end
# VRFS-1976
it "create two sessions; select the second one" do
# this is tricky because of iCheck and it's custom way of dealing with 'checked' state
start1 = 15.minutes.ago
session1 = FactoryGirl.create(:music_session, description: 'My Session 1', creator: user1, scheduled_start: start1)
session2 = FactoryGirl.create(:music_session, description: 'My Session 2', creator: user1)
in_client(user1) do
# should reload the session page, so that we see new sessions
visit '/client#/createSession'
# pick the second session
page.find("#scheduled-session-list input[data-session-id='#{session2.id}'] + ins").trigger(:click)
page.find('.btn-next').trigger(:click)
# check if future dialog is showing; if so, accept
accept = first('#btn-confirm-ok', text: "START SESSION NOW")
accept.trigger(:click) if accept
page.find('#session-description-disp', text: 'My Session 2')
end
end
describe "step 1" do
it "initial status" do
in_client(user1) do

View File

@ -62,12 +62,13 @@ describe "Create Session", :js => true, :type => :feature, :capybara_feature =>
let (:first_session) { FactoryGirl.create(:music_session, creator: user1, scheduled_start: now + 75.minutes) }
it "warns the user that session starts in the future, and user can start session" do
pending "play with Database.db_timezone on this one"
sleep 1
find('.btn-next').trigger(:click)
sleep 2
expect(page).to have_selector('h1', text: 'Future Session')
expect(page).to have_content "Are you sure"
find('#btn-confirm-ok', text: 'Start Session Now').trigger(:click)
find('#btn-confirm-ok', text: 'START SESSION NOW').trigger(:click)
sleep 1
expect(page).to have_selector('.session-step-title', text: 'Review & Confirm')
expect(page).to have_content first_session.name

View File

@ -10,6 +10,22 @@ describe "Feed", :js => true, :type => :feature, :capybara_feature => true do
end
describe "regressions" do
describe "mount" do
it "should render when has mount" do
# regression for VRFS-1987
ams = FactoryGirl.create(:active_music_session)
FactoryGirl.create(:icecast_mount, music_session_id: ams.id)
fast_signin user, "/client#/feed"
find('#feedScreen')
find(".feed-entry.music-session-history-entry[data-music-session='#{ams.id}']")
end
end
end
describe "sessions" do
before(:each) do
@ -40,6 +56,7 @@ describe "Feed", :js => true, :type => :feature, :capybara_feature => true do
# it " and render artist hover bubble"
# end
it "should render stats" do
visit "/client#/feed"

View File

@ -8,6 +8,7 @@ describe "Profile History", :js => true, :type => :feature, :capybara_feature =>
before do
MusicSession.delete_all
ActiveMusicSession.delete_all
Recording.delete_all
set_login_cookie user
stub_const("APP_CONFIG", web_config)

View File

@ -207,6 +207,10 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
# musician_access = true, approval_required = false
it "should allow anyone to view for 'at will' option after session starts" do
@music_session.musician_access = true
@music_session.approval_required = false
@music_session.save!
# attempt to access with musician who was invited but didn't RSVP
fast_signin(@session_invitee, @url)
ensure_success({:show_cta => true, :button_text => 'RSVP NOW!'})
@ -235,6 +239,10 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
# musician_access = true, approval_required = true
it "should allow anyone to view for 'join by approval' option after session starts" do
@music_session.musician_access = true
@music_session.approval_required = true
@music_session.save!
# attempt to access with musician who was invited but didn't RSVP
fast_signin(@session_invitee, @url)
ensure_success({:show_cta => true, :button_text => 'RSVP NOW!'})

View File

@ -7,12 +7,21 @@ describe "Signup", :js => true, :type => :feature, :capybara_feature => true do
before(:each) do
@mac_client = FactoryGirl.create(:artifact_update)
UserMailer.deliveries.clear
MaxMindManager.create_phony_database
end
describe "signup page" do
before { visit signup_path }
it { should have_selector('h2', text: 'Create a JamKazam account') }
it "should initialize successfully" do
should have_selector('h2', text: 'Create a JamKazam account')
# we should see these locations in the signup form already chosen
location = GeoIpLocations.lookup('127.0.0.1')
find('.field.country .easydropdown .selected', text:location[:country])
find('.field.state .easydropdown .selected', text:location[:state])
find('.field.city .easydropdown .selected', text:location[:city])
end
describe "with valid musician information" do
before do
@ -30,7 +39,12 @@ describe "Signup", :js => true, :type => :feature, :capybara_feature => true do
it {
should have_title("JamKazam | Congratulations")
should have_content("You have successfully registered as a JamKazam musician.")
User.find_by_email('newuser1@jamkazam.com').musician_instruments.length.should == 1
user = User.find_by_email('newuser1@jamkazam.com')
user.musician_instruments.length.should == 1
location = GeoIpLocations.lookup('127.0.0.1')
user.country.should == location[:country]
user.state.should == location[:state]
user.city.should == location[:city]
# an email is sent on no-invite signup
UserMailer.deliveries.length.should == 1
UserMailer.deliveries[0].html_part.body.include?("To confirm this email address")== 1
@ -45,10 +59,10 @@ describe "Signup", :js => true, :type => :feature, :capybara_feature => true do
end
it {
should have_title("JamKazam")
should have_selector('h2', text: "musicians")
should have_title("Signup Confirmation")
should have_selector('h1', text: "Email Confirmed")
UserMailer.deliveries.length.should == 1
UserMailer.deliveries[0].html_part.body.include?("Following are links to some resources")== 1
UserMailer.deliveries[0].html_part.body.include?("Following are links to some resources") == 1
}
end
end

BIN
web/spec/files/avatar.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -61,7 +61,7 @@ describe "Musician Search API", :type => :api do
it "gets musicians for long-distance locations" do
get_query({score_limit: Search::GOOD_SCORE})
good_response
(json['musicians'] || []).count.should == 5
(json['musicians'] || []).count.should == 3 # only users that have the 10 latency are low enough
end
end
@ -106,7 +106,7 @@ describe "Musician Search API", :type => :api do
f5.save
expect(@user4.followers.count).to be 3
get_query
get_query(orderby: :followed)
good_response
musician = json["musicians"][0]
expect(musician["id"]).to eq(@user4.id)

View File

@ -6,13 +6,18 @@ def bputs(msg)
end
end
bputs "before simplecov"
require 'simplecov'
bputs "before rubygems"
require 'rubygems'
bputs "before omniauth"
#require 'spork'
require 'omniauth'
#uncomment the following line to use spork with the debugger
#require 'spork/ext/ruby-debug'
ENV["RAILS_ENV"] ||= 'test'
bputs "before activerecord load"
@ -28,7 +33,6 @@ bputs "before db_config load"
db_config = YAML::load(File.open('config/database.yml'))["test"]
# initialize ActiveRecord's db connection\
bputs "before recreate db"
SpecDb::recreate_database(db_config)

View File

@ -54,6 +54,22 @@ def web_config
def max_track_part_upload_failures
3
end
def icecast_hardcoded_source_password
'blueberryjam'
end
def max_good_full_score
20
end
def max_yellow_full_score
35
end
def max_red_full_score
50
end
end
klass.new
end

View File

@ -543,7 +543,7 @@ def change_session_genre #randomly just change it
find('#session-settings-dialog') # ensure the dialog is visible
within('#session-settings-dialog') do
wait_for_ajax
@new_genre = get_options(here).-(["Select Genre"]).-(selected_genres).sample.to_s
@new_genre = get_options(here).-(["Select Genre", "Unspecified"]).-(selected_genres).sample.to_s
jk_select(@new_genre, '#session-settings-dialog select[name="genres"]')
wait_for_ajax
find('#session-settings-dialog-submit').trigger(:click)

View File

@ -1,462 +1,509 @@
/*!
* iCheck v0.9.1, http://git.io/uhUPMA
* =================================
* Powerful jQuery plugin for checkboxes and radio buttons customization
* iCheck v1.0.2, http://git.io/arlzeA
* ===================================
* Powerful jQuery and Zepto plugin for checkboxes and radio buttons customization
*
* (c) 2013 Damir Foy, http://damirfoy.com
* (c) 2013 Damir Sultanov, http://fronteed.com
* MIT Licensed
*/
(function($) {
// Cached vars
var _iCheck = 'iCheck',
_iCheckHelper = _iCheck + '-helper',
_checkbox = 'checkbox',
_radio = 'radio',
_checked = 'checked',
_unchecked = 'un' + _checked,
_disabled = 'disabled',
_determinate = 'determinate',
_indeterminate = 'in' + _determinate,
_update = 'update',
_type = 'type',
_click = 'click',
_touch = 'touchbegin.i touchend.i',
_add = 'addClass',
_remove = 'removeClass',
_callback = 'trigger',
_label = 'label',
_cursor = 'cursor',
_mobile = /ipad|iphone|ipod|android|blackberry|windows phone|opera mini|silk/i.test(navigator.userAgent);
// Cached vars
var _iCheck = 'iCheck',
_iCheckHelper = _iCheck + '-helper',
_checkbox = 'checkbox',
_radio = 'radio',
_checked = 'checked',
_unchecked = 'un' + _checked,
_disabled = 'disabled',
_determinate = 'determinate',
_indeterminate = 'in' + _determinate,
_update = 'update',
_type = 'type',
_click = 'click',
_touch = 'touchbegin.i touchend.i',
_add = 'addClass',
_remove = 'removeClass',
_callback = 'trigger',
_label = 'label',
_cursor = 'cursor',
_mobile = /ipad|iphone|ipod|android|blackberry|windows phone|opera mini|silk/i.test(navigator.userAgent);
// Plugin init
$.fn[_iCheck] = function(options, fire) {
// Plugin init
$.fn[_iCheck] = function(options, fire) {
// Walker
var handle = ':' + _checkbox + ', :' + _radio,
stack = $(),
walker = function(object) {
object.each(function() {
var self = $(this);
// Walker
var handle = 'input[type="' + _checkbox + '"], input[type="' + _radio + '"]',
stack = $(),
walker = function(object) {
object.each(function() {
var self = $(this);
if (self.is(handle)) {
stack = stack.add(self);
} else {
stack = stack.add(self.find(handle));
};
});
};
if (self.is(handle)) {
stack = stack.add(self);
} else {
stack = stack.add(self.find(handle));
}
});
};
// Check if we should operate with some method
if (/^(check|uncheck|toggle|indeterminate|determinate|disable|enable|update|destroy)$/i.test(options)) {
// Check if we should operate with some method
if (/^(check|uncheck|toggle|indeterminate|determinate|disable|enable|update|destroy)$/i.test(options)) {
// Normalize method's name
options = options.toLowerCase();
// Normalize method's name
options = options.toLowerCase();
// Find checkboxes and radio buttons
walker(this);
// Find checkboxes and radio buttons
walker(this);
return stack.each(function() {
if (options == 'destroy') {
tidy(this, 'ifDestroyed');
} else {
operate($(this), true, options);
};
return stack.each(function() {
var self = $(this);
// Fire method's callback
if ($.isFunction(fire)) {
fire();
};
});
// Customization
} else if (typeof options == 'object' || !options) {
// Check if any options were passed
var settings = $.extend({
checkedClass: _checked,
disabledClass: _disabled,
indeterminateClass: _indeterminate,
labelHover: true
}, options),
selector = settings.handle,
hoverClass = settings.hoverClass || 'hover',
focusClass = settings.focusClass || 'focus',
activeClass = settings.activeClass || 'active',
labelHover = !!settings.labelHover,
labelHoverClass = settings.labelHoverClass || 'hover',
// Setup clickable area
area = ('' + settings.increaseArea).replace('%', '') | 0;
// Selector limit
if (selector == _checkbox || selector == _radio) {
handle = ':' + selector;
};
// Clickable area limit
if (area < -50) {
area = -50;
};
// Walk around the selector
walker(this);
return stack.each(function() {
// If already customized
tidy(this);
var self = $(this),
node = this,
id = node.id,
// Layer styles
offset = -area + '%',
size = 100 + (area * 2) + '%',
layer = {
position: 'absolute',
top: offset,
left: offset,
display: 'block',
width: size,
height: size,
margin: 0,
padding: 0,
background: '#fff',
border: 0,
opacity: 0
},
// Choose how to hide input
hide = _mobile ? {
position: 'absolute',
visibility: 'hidden'
} : area ? layer : {
position: 'absolute',
opacity: 0
},
// Get proper class
className = node[_type] == _checkbox ? settings.checkboxClass || 'i' + _checkbox : settings.radioClass || 'i' + _radio,
// Find assigned labels
label = $(_label + '[for="' + id + '"]').add(self.closest(_label)),
// Wrap input
parent = self.wrap('<div class="' + className + '"/>')[_callback]('ifCreated').parent().append(settings.insert),
// Layer addition
helper = $('<ins class="' + _iCheckHelper + '"/>').css(layer).appendTo(parent);
// Finalize customization
self.data(_iCheck, {o: settings, s: self.attr('style')}).css(hide);
!!settings.inheritClass && parent[_add](node.className);
!!settings.inheritID && id && parent.attr('id', _iCheck + '-' + id);
parent.css('position') == 'static' && parent.css('position', 'relative');
operate(self, true, _update);
// Label events
if (label.length) {
label.on(_click + '.i mouseenter.i mouseleave.i ' + _touch, function(event) {
var type = event[_type],
item = $(this);
// Do nothing if input is disabled
if (!node[_disabled]) {
// Click
if (type == _click) {
operate(self, false, true);
// Hover state
} else if (labelHover) {
// mouseleave|touchend
if (/ve|nd/.test(type)) {
parent[_remove](hoverClass);
item[_remove](labelHoverClass);
} else {
parent[_add](hoverClass);
item[_add](labelHoverClass);
};
};
if (_mobile) {
event.stopPropagation();
} else {
return false;
};
};
});
};
// Input events
self.on(_click + '.i focus.i blur.i keyup.i keydown.i keypress.i', function(event) {
var type = event[_type],
key = event.keyCode;
// Click
if (type == _click) {
return false;
// Keydown
} else if (type == 'keydown' && key == 32) {
if (!(node[_type] == _radio && node[_checked])) {
if (node[_checked]) {
off(self, _checked);
} else {
on(self, _checked);
};
};
return false;
// Keyup
} else if (type == 'keyup' && node[_type] == _radio) {
!node[_checked] && on(self, _checked);
// Focus/blur
} else if (/us|ur/.test(type)) {
parent[type == 'blur' ? _remove : _add](focusClass);
};
});
// Helper events
helper.on(_click + ' mousedown mouseup mouseover mouseout ' + _touch, function(event) {
var type = event[_type],
// mousedown|mouseup
toggle = /wn|up/.test(type) ? activeClass : hoverClass;
// Do nothing if input is disabled
if (!node[_disabled]) {
// Click
if (type == _click) {
operate(self, false, true);
// Active and hover states
} else {
// State is on
if (/wn|er|in/.test(type)) {
// mousedown|mouseover|touchbegin
parent[_add](toggle);
// State is off
} else {
parent[_remove](toggle + ' ' + activeClass);
};
// Label hover
if (label.length && labelHover && toggle == hoverClass) {
// mouseout|touchend
label[/ut|nd/.test(type) ? _remove : _add](labelHoverClass);
};
};
if (_mobile) {
event.stopPropagation();
} else {
return false;
};
};
});
});
if (options == 'destroy') {
tidy(self, 'ifDestroyed');
} else {
return this;
};
};
operate(self, true, options);
}
// Do something with inputs
function operate(input, direct, method) {
var node = input[0];
state = /er/.test(method) ? _indeterminate : /bl/.test(method) ? _disabled : _checked,
active = method == _update ? {
checked: node[_checked],
disabled: node[_disabled],
indeterminate: input.attr(_indeterminate) == 'true' || input.attr(_determinate) == 'false'
} : node[state];
// Fire method's callback
if ($.isFunction(fire)) {
fire();
}
});
// Check, disable or indeterminate
if (/^(ch|di|in)/.test(method) && !active) {
on(input, state);
// Customization
} else if (typeof options == 'object' || !options) {
// Uncheck, enable or determinate
} else if (/^(un|en|de)/.test(method) && active) {
off(input, state);
// Check if any options were passed
var settings = $.extend({
checkedClass: _checked,
disabledClass: _disabled,
indeterminateClass: _indeterminate,
labelHover: true
}, options),
// Update
} else if (method == _update) {
selector = settings.handle,
hoverClass = settings.hoverClass || 'hover',
focusClass = settings.focusClass || 'focus',
activeClass = settings.activeClass || 'active',
labelHover = !!settings.labelHover,
labelHoverClass = settings.labelHoverClass || 'hover',
// Handle states
for (var state in active) {
if (active[state]) {
on(input, state, true);
// Setup clickable area
area = ('' + settings.increaseArea).replace('%', '') | 0;
// Selector limit
if (selector == _checkbox || selector == _radio) {
handle = 'input[type="' + selector + '"]';
}
// Clickable area limit
if (area < -50) {
area = -50;
}
// Walk around the selector
walker(this);
return stack.each(function() {
var self = $(this);
// If already customized
tidy(self);
var node = this,
id = node.id,
// Layer styles
offset = -area + '%',
size = 100 + (area * 2) + '%',
layer = {
position: 'absolute',
top: offset,
left: offset,
display: 'block',
width: size,
height: size,
margin: 0,
padding: 0,
background: '#fff',
border: 0,
opacity: 0
},
// Choose how to hide input
hide = _mobile ? {
position: 'absolute',
visibility: 'hidden'
} : area ? layer : {
position: 'absolute',
opacity: 0
},
// Get proper class
className = node[_type] == _checkbox ? settings.checkboxClass || 'i' + _checkbox : settings.radioClass || 'i' + _radio,
// Find assigned labels
label = $(_label + '[for="' + id + '"]').add(self.closest(_label)),
// Check ARIA option
aria = !!settings.aria,
// Set ARIA placeholder
ariaID = _iCheck + '-' + Math.random().toString(36).substr(2,6),
// Parent & helper
parent = '<div class="' + className + '" ' + (aria ? 'role="' + node[_type] + '" ' : ''),
helper;
// Set ARIA "labelledby"
if (aria) {
label.each(function() {
parent += 'aria-labelledby="';
if (this.id) {
parent += this.id;
} else {
this.id = ariaID;
parent += ariaID;
}
parent += '"';
});
}
// Wrap input
parent = self.wrap(parent + '/>')[_callback]('ifCreated').parent().append(settings.insert);
// Layer addition
helper = $('<ins class="' + _iCheckHelper + '"/>').css(layer).appendTo(parent);
// Finalize customization
self.data(_iCheck, {o: settings, s: self.attr('style')}).css(hide);
!!settings.inheritClass && parent[_add](node.className || '');
!!settings.inheritID && id && parent.attr('id', _iCheck + '-' + id);
parent.css('position') == 'static' && parent.css('position', 'relative');
operate(self, true, _update);
// Label events
if (label.length) {
label.on(_click + '.i mouseover.i mouseout.i ' + _touch, function(event) {
var type = event[_type],
item = $(this);
// Do nothing if input is disabled
if (!node[_disabled]) {
// Click
if (type == _click) {
if ($(event.target).is('a')) {
return;
}
operate(self, false, true);
// Hover state
} else if (labelHover) {
// mouseout|touchend
if (/ut|nd/.test(type)) {
parent[_remove](hoverClass);
item[_remove](labelHoverClass);
} else {
off(input, state, true);
};
};
parent[_add](hoverClass);
item[_add](labelHoverClass);
}
}
} else if (!direct || method == 'toggle') {
if (_mobile) {
event.stopPropagation();
} else {
return false;
}
}
});
}
// Helper or label was clicked
if (!direct) {
input[_callback]('ifClicked');
};
// Input events
self.on(_click + '.i focus.i blur.i keyup.i keydown.i keypress.i', function(event) {
var type = event[_type],
key = event.keyCode;
// Toggle checked state
if (active) {
if (node[_type] !== _radio) {
off(input, state);
};
} else {
on(input, state);
};
};
};
// Click
if (type == _click) {
return false;
// Add checked, disabled or indeterminate state
function on(input, state, keep) {
var node = input[0],
parent = input.parent(),
checked = state == _checked,
indeterminate = state == _indeterminate,
callback = indeterminate ? _determinate : checked ? _unchecked : 'enabled',
regular = option(node, callback + capitalize(node[_type])),
specific = option(node, state + capitalize(node[_type]));
// Keydown
} else if (type == 'keydown' && key == 32) {
if (!(node[_type] == _radio && node[_checked])) {
if (node[_checked]) {
off(self, _checked);
} else {
on(self, _checked);
}
}
// Prevent unnecessary actions
if (node[state] !== true) {
return false;
// Toggle assigned radio buttons
if (!keep && state == _checked && node[_type] == _radio && node.name) {
var form = input.closest('form'),
inputs = 'input[name="' + node.name + '"]';
// Keyup
} else if (type == 'keyup' && node[_type] == _radio) {
!node[_checked] && on(self, _checked);
inputs = form.length ? form.find(inputs) : $(inputs);
// Focus/blur
} else if (/us|ur/.test(type)) {
parent[type == 'blur' ? _remove : _add](focusClass);
}
});
inputs.each(function() {
if (this !== node && $.data(this, _iCheck)) {
off($(this), state);
};
});
};
// Helper events
helper.on(_click + ' mousedown mouseup mouseover mouseout ' + _touch, function(event) {
var type = event[_type],
// Indeterminate state
if (indeterminate) {
// mousedown|mouseup
toggle = /wn|up/.test(type) ? activeClass : hoverClass;
// Add indeterminate state
node[state] = true;
// Do nothing if input is disabled
if (!node[_disabled]) {
// Remove checked state
if (node[_checked]) {
off(input, _checked, 'force');
};
// Click
if (type == _click) {
operate(self, false, true);
// Checked or disabled state
// Active and hover states
} else {
// Add checked or disabled state
if (!keep) {
node[state] = true;
};
// State is on
if (/wn|er|in/.test(type)) {
// Remove indeterminate state
if (checked && node[_indeterminate]) {
off(input, _indeterminate, false);
};
};
// mousedown|mouseover|touchbegin
parent[_add](toggle);
// Trigger callbacks
callbacks(input, checked, state, keep);
};
// State is off
} else {
parent[_remove](toggle + ' ' + activeClass);
}
// Add proper cursor
if (node[_disabled] && !!option(node, _cursor, true)) {
parent.find('.' + _iCheckHelper).css(_cursor, 'default');
};
// Label hover
if (label.length && labelHover && toggle == hoverClass) {
// Add state class
parent[_add](specific || option(node, state));
// mouseout|touchend
label[/ut|nd/.test(type) ? _remove : _add](labelHoverClass);
}
}
// Remove regular state class
parent[_remove](regular || option(node, callback) || '');
};
if (_mobile) {
event.stopPropagation();
} else {
return false;
}
}
});
});
} else {
return this;
}
};
// Remove checked, disabled or indeterminate state
function off(input, state, keep) {
var node = input[0],
parent = input.parent(),
checked = state == _checked,
indeterminate = state == _indeterminate,
callback = indeterminate ? _determinate : checked ? _unchecked : 'enabled',
regular = option(node, callback + capitalize(node[_type])),
specific = option(node, state + capitalize(node[_type]));
// Do something with inputs
function operate(input, direct, method) {
var node = input[0],
state = /er/.test(method) ? _indeterminate : /bl/.test(method) ? _disabled : _checked,
active = method == _update ? {
checked: node[_checked],
disabled: node[_disabled],
indeterminate: input.attr(_indeterminate) == 'true' || input.attr(_determinate) == 'false'
} : node[state];
// Prevent unnecessary actions
if (node[state] !== false) {
// Check, disable or indeterminate
if (/^(ch|di|in)/.test(method) && !active) {
on(input, state);
// Toggle state
if (indeterminate || !keep || keep == 'force') {
node[state] = false;
};
// Uncheck, enable or determinate
} else if (/^(un|en|de)/.test(method) && active) {
off(input, state);
// Trigger callbacks
callbacks(input, checked, callback, keep);
};
// Update
} else if (method == _update) {
// Add proper cursor
if (!node[_disabled] && !!option(node, _cursor, true)) {
parent.find('.' + _iCheckHelper).css(_cursor, 'pointer');
};
// Handle states
for (var each in active) {
if (active[each]) {
on(input, each, true);
} else {
off(input, each, true);
}
}
// Remove state class
parent[_remove](specific || option(node, state) || '');
} else if (!direct || method == 'toggle') {
// Add regular state class
parent[_add](regular || option(node, callback));
};
// Helper or label was clicked
if (!direct) {
input[_callback]('ifClicked');
}
// Remove all traces
function tidy(node, callback) {
if ($.data(node, _iCheck)) {
var input = $(node);
// Toggle checked state
if (active) {
if (node[_type] !== _radio) {
off(input, state);
}
} else {
on(input, state);
}
}
}
// Remove everything except input
input.parent().html(input.attr('style', $.data(node, _iCheck).s || '')[_callback](callback || ''));
// Add checked, disabled or indeterminate state
function on(input, state, keep) {
var node = input[0],
parent = input.parent(),
checked = state == _checked,
indeterminate = state == _indeterminate,
disabled = state == _disabled,
callback = indeterminate ? _determinate : checked ? _unchecked : 'enabled',
regular = option(input, callback + capitalize(node[_type])),
specific = option(input, state + capitalize(node[_type]));
// Unbind events
input.off('.i').unwrap();
$(_label + '[for="' + node.id + '"]').add(input.closest(_label)).off('.i');
};
};
// Prevent unnecessary actions
if (node[state] !== true) {
// Get some option
function option(node, state, regular) {
if ($.data(node, _iCheck)) {
return $.data(node, _iCheck).o[state + (regular ? '' : 'Class')];
};
};
// Toggle assigned radio buttons
if (!keep && state == _checked && node[_type] == _radio && node.name) {
var form = input.closest('form'),
inputs = 'input[name="' + node.name + '"]';
// Capitalize some string
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
inputs = form.length ? form.find(inputs) : $(inputs);
// Executable handlers
function callbacks(input, checked, callback, keep) {
inputs.each(function() {
if (this !== node && $(this).data(_iCheck)) {
off($(this), state);
}
});
}
// Indeterminate state
if (indeterminate) {
// Add indeterminate state
node[state] = true;
// Remove checked state
if (node[_checked]) {
off(input, _checked, 'force');
}
// Checked or disabled state
} else {
// Add checked or disabled state
if (!keep) {
if (checked) {
input[_callback]('ifToggled');
};
node[state] = true;
}
input[_callback]('ifChanged')[_callback]('if' + capitalize(callback));
};
};
})(jQuery);
// Remove indeterminate state
if (checked && node[_indeterminate]) {
off(input, _indeterminate, false);
}
}
// Trigger callbacks
callbacks(input, checked, state, keep);
}
// Add proper cursor
if (node[_disabled] && !!option(input, _cursor, true)) {
parent.find('.' + _iCheckHelper).css(_cursor, 'default');
}
// Add state class
parent[_add](specific || option(input, state) || '');
// Set ARIA attribute
if (!!parent.attr('role') && !indeterminate) {
parent.attr('aria-' + (disabled ? _disabled : _checked), 'true');
}
// Remove regular state class
parent[_remove](regular || option(input, callback) || '');
}
// Remove checked, disabled or indeterminate state
function off(input, state, keep) {
var node = input[0],
parent = input.parent(),
checked = state == _checked,
indeterminate = state == _indeterminate,
disabled = state == _disabled,
callback = indeterminate ? _determinate : checked ? _unchecked : 'enabled',
regular = option(input, callback + capitalize(node[_type])),
specific = option(input, state + capitalize(node[_type]));
// Prevent unnecessary actions
if (node[state] !== false) {
// Toggle state
if (indeterminate || !keep || keep == 'force') {
node[state] = false;
}
// Trigger callbacks
callbacks(input, checked, callback, keep);
}
// Add proper cursor
if (!node[_disabled] && !!option(input, _cursor, true)) {
parent.find('.' + _iCheckHelper).css(_cursor, 'pointer');
}
// Remove state class
parent[_remove](specific || option(input, state) || '');
// Set ARIA attribute
if (!!parent.attr('role') && !indeterminate) {
parent.attr('aria-' + (disabled ? _disabled : _checked), 'false');
}
// Add regular state class
parent[_add](regular || option(input, callback) || '');
}
// Remove all traces
function tidy(input, callback) {
if (input.data(_iCheck)) {
// Remove everything except input
input.parent().html(input.attr('style', input.data(_iCheck).s || ''));
// Callback
if (callback) {
input[_callback](callback);
}
// Unbind events
input.off('.i').unwrap();
$(_label + '[for="' + input[0].id + '"]').add(input.closest(_label)).off('.i');
}
}
// Get some option
function option(input, state, regular) {
if (input.data(_iCheck)) {
return input.data(_iCheck).o[state + (regular ? '' : 'Class')];
}
}
// Capitalize some string
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// Executable handlers
function callbacks(input, checked, callback, keep) {
if (!keep) {
if (checked) {
input[_callback]('ifToggled');
}
input[_callback]('ifChanged')[_callback]('if' + capitalize(callback));
}
}
})(window.jQuery || window.Zepto);