* VRFS-1976 - fixed issue where UI always picks 1st scheduled session; VRFS-1962 - use 'full latency' instead of just internet latency in musician search, find session, and new musicians email

This commit is contained in:
Seth Call 2014-07-29 17:52:20 -05:00
parent 7e810809ac
commit c3461e82a7
22 changed files with 804 additions and 479 deletions

View File

@ -197,4 +197,5 @@ update_sms_index.sql
connection_allow_null_locidispid.sql
track_user_in_scores.sql
median_aggregate.sql
current_scores_use_median.sql
current_scores_use_median.sql
current_scores_ams_index_sms_index_use_user_instrument.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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -67,7 +67,7 @@ describe "Create Session", :js => true, :type => :feature, :capybara_feature =>
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

@ -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,8 +106,9 @@ describe "Musician Search API", :type => :api do
f5.save
expect(@user4.followers.count).to be 3
get_query
get_query(orderby: :followed)
good_response
puts last_response.body
musician = json["musicians"][0]
expect(musician["id"]).to eq(@user4.id)
followings = musician['followings']

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