Merge branch 'feature/scheduled_sessions' of https://bitbucket.org/jamkazam/jam-cloud into feature/scheduled_sessions

This commit is contained in:
Bert Owen 2014-06-14 20:52:47 +08:00
commit 88a7ba197a
8 changed files with 292 additions and 78 deletions

62
db/up/ams_index.sql Normal file
View File

@ -0,0 +1,62 @@
-- DROP FUNCTION IF EXISTS ams_index (my_user_id VARCHAR, my_locidispid BIGINT, my_audio_latency INTEGER, poff INTEGER, plim INTEGER);
CREATE OR REPLACE FUNCTION ams_index (my_user_id VARCHAR, my_locidispid BIGINT, my_audio_latency INTEGER)
RETURNS VOID
LANGUAGE plpgsql
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) NOT NULL, user_id VARCHAR(64) NOT NULL, latency INTEGER NOT NULL) ON COMMIT DROP;
-- populate ams_users_tmp as all musicians in the ams_music_session_tmp sessions with audio latency and score
INSERT INTO ams_users_tmp SELECT c.music_session_id, c.user_id, (s.score+my_audio_latency+c.last_jam_audio_latency)/2 AS latency
FROM ams_music_session_tmp q, connections c, current_scores s WHERE
q.music_session_id = c.music_session_id AND
c.locidispid = s.alocidispid AND
s.blocidispid = my_locidispid AND
c.last_jam_audio_latency IS NOT NULL;
-- 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;
$$;

View File

@ -314,6 +314,85 @@ module JamRuby
return query
end
# Generate a list of music sessions (that are active) filtered by genre, language, keyword, and sorted
# (and tagged) by rsvp'd (1st), invited (2nd), and musician can join (3rd). within a group
# tagged the same, sorted by score. date seems irrelevant as these are active sessions.
def self.ams_index(current_user, options = {})
client_id = options[:client_id]
genre = options[:genre]
lang = options[:lang]
keyword = options[:keyword]
offset = options[:offset]
limit = options[:limit]
connection = Connection.where(user_id: current_user.id, client_id: client_id).first!
my_locidispid = connection.locidispid
my_audio_latency = connection.last_jam_audio_latency
query = MusicSession
.select('music_sessions.*')
# TODO this is not really needed when ams_music_session_tmp is joined
# unless there is something specific we need out of active_music_sessions
query = query.joins(
%Q{
INNER JOIN
active_music_sessions
ON
active_music_sessions.id = music_sessions.id
}
)
.select('1::integer as tag, 15::integer as latency')
# TODO integrate ams_music_session_tmp into the processing
# then we can join ams_music_session_tmp and not join active_music_sessions
# query = query.joins(
# %Q{
# INNER JOIN
# ams_music_session_tmp
# ON
# ams_music_session_tmp.music_session_id = active_music_sessions.id
# }
# )
# .select('ams_music_session_tmp.tag, ams_music_session_tmp.latency')
query = query.order(
%Q{
tag, latency, music_sessions.id
}
)
.group(
%Q{
tag, latency, music_sessions.id
}
)
if (offset)
query = query.offset(offset)
end
if (limit)
query = query.limit(limit)
end
# cleanse keyword so it is only word characters. ignore if less than 3 characters long or greater than 100
# TODO do we want to force match of whole words only? this matches any substring...
# TODO do we want to match more than one word in the phrase? this matches only the first word...
unless keyword.nil? or keyword.length < 3 or keyword.length > 100
w = keyword.split(/\W+/)
if w.length > 0 and w.length >= 3
query = query.where("music_sessions.description like '%#{w[0]}%'")
end
end
query = query.where("music_sessions.genre_id = ?", genre) unless genre.nil?
# TODO filter by lang
return query
end
def self.participant_create user, music_session_id, client_id, as_musician, tracks
music_session = MusicSession.find(music_session_id)

View File

@ -154,7 +154,7 @@ module JamRuby
raise StateError, "Slot does not exist"
end
if rsvp_slot.chosen
if rsvp_slot.chosen && r[:accept]
raise StateError, "The #{rsvp_slot.instrument_id} slot has already been approved for another user."
end

View File

@ -332,6 +332,36 @@ describe ActiveMusicSession do
end
end
describe "ams_index" do
it "does not crash" do
creator = FactoryGirl.create(:user)
creator2 = FactoryGirl.create(:user)
earlier_session = FactoryGirl.create(:active_music_session, :creator => creator, :description => "Earlier Session")
c1 = FactoryGirl.create(:connection, user: creator, music_session: earlier_session, addr: 0x01020304, locidispid: 1)
later_session = FactoryGirl.create(:active_music_session, :creator => creator2, :description => "Later Session")
c2 = FactoryGirl.create(:connection, user: creator2, music_session: later_session, addr: 0x21020304, locidispid: 2)
user = FactoryGirl.create(:user)
c3 = FactoryGirl.create(:connection, user: user, locidispid: 3)
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);
music_sessions = ActiveMusicSession.ams_index(user, client_id: c3.client_id).take(100)
music_sessions.should_not be_nil
music_sessions.length.should == 2
end
# todo we need more tests:
# rsvp'd user (chosen and not chosen)
# invited user
# creator (who should be automatically rsvp'd)
# musician_access and not
end
it 'uninvited users cant join approval-required sessions without invitation' do
user1 = FactoryGirl.create(:user) # in the jam session

View File

@ -83,18 +83,33 @@
rest.getRsvpRequests(musicSessionId)
.done(function(rsvps) {
if (rsvps && rsvps.length > 0) {
// should only be 1 RSVP for this session
// should only be 1 RSVP for this session and user
var rsvp = rsvps[0];
if (rsvp.canceled) {
$('.call-to-action').html('Your RSVP request to this session has been cancelled.');
$btnAction.hide();
}
else {
$('.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);
});
var declined = true;
if (rsvp.rsvp_requests_rsvp_slots) {
for (var x=0; x < rsvp.rsvp_requests_rsvp_slots.length; x++) {
if (rsvp.rsvp_requests_rsvp_slots[x].chosen) {
declined = false;
}
}
}
if (declined) {
$('.call-to-action').html('Your RSVP request to this session has been declined.');
$btnAction.hide();
}
else {
$('.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);
});
}
}
}
// no RSVP

View File

@ -60,6 +60,21 @@ class ApiMusicSessionsController < ApiController
limit: params[:limit])
end
def ams_index
# returns a relation which will produce a list of music_sessions which are active and augmented with attributes
# tag and latency, then sorted by tag, latency, and finally music_sessions.id (for stability). the list is
# filtered by genre, lang, and keyword, then paged by offset and limit (those are record numbers not page numbers).
# tag is 1 for chosen rsvp'd sessions, 2 for invited sessions, 3 for all others (musician_access). if you're the
# creator of a session it will be treated the same as if you had rsvp'd and been accepted.
@music_sessions = ActiveMusicSession.ams_index(current_user,
client_id: params[:client_id],
genre: params[:genre],
lang: params[:lang],
keyword: params[:keyword],
offset: params[:offset],
limit: params[:limit])
end
def scheduled
@music_sessions = MusicSession.scheduled(current_user)
end

View File

@ -1,15 +1,15 @@
object @rsvp_request
attributes :id, :canceled, :created_at
attributes :id, :canceled, :cancel_all, :created_at
child(:user => :user) {
attributes :id, :name, :photo_url
}
child(:rsvp_slots => :rsvp_slots) {
attributes :id, :instrument_id, :proficiency_level, :music_session_id
child(:rsvp_requests_rsvp_slots => :rsvp_requests_rsvp_slots) {
attributes :id, :chosen
child(:rsvp_requests_rsvp_slots => :rsvp_requests_rsvp_slots) {
attributes :id, :chosen
child(:rsvp_slot => :rsvp_slot) {
attributes :id, :instrument_id, :proficiency_level, :music_session_id
}
}

View File

@ -12,8 +12,11 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
MusicSession.delete_all
User.delete_all
@rsvp_user = FactoryGirl.create(:user)
@rsvp_user.save
@rsvp_approved_user = FactoryGirl.create(:user)
@rsvp_approved_user.save
@rsvp_declined_user = FactoryGirl.create(:user)
@rsvp_declined_user.save
@session_invitee = FactoryGirl.create(:user)
@session_invitee.save
@ -28,12 +31,17 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
FactoryGirl.create(:friendship, :user => @session_invitee, :friend => @session_creator)
FactoryGirl.create(:friendship, :user => @session_creator, :friend => @session_invitee)
FactoryGirl.create(:friendship, :user => @rsvp_user, :friend => @session_creator)
FactoryGirl.create(:friendship, :user => @session_creator, :friend => @rsvp_user)
FactoryGirl.create(:friendship, :user => @rsvp_approved_user, :friend => @session_creator)
FactoryGirl.create(:friendship, :user => @session_creator, :friend => @rsvp_approved_user)
@music_session = FactoryGirl.build(:music_session, :creator => @session_creator)
FactoryGirl.create(:friendship, :user => @rsvp_declined_user, :friend => @session_creator)
FactoryGirl.create(:friendship, :user => @session_creator, :friend => @rsvp_declined_user)
@music_session = FactoryGirl.build(:music_session, :creator => @session_creator, :scheduled_start => Time.now.utc + 2.days)
@music_session.save
@url = "/sessions/#{@music_session.id}/details"
@slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('electric guitar'))
@slot1.save
@ -43,17 +51,27 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
@invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @session_invitee, :music_session => @music_session)
@invitation.save
@invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @rsvp_user, :music_session => @music_session)
@invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @rsvp_approved_user, :music_session => @music_session)
@invitation.save
# create RSVP request
rsvp = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "Let's Jam!"}, @rsvp_user)
@invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @rsvp_declined_user, :music_session => @music_session)
@invitation.save
# create RSVP request 1
@rsvp1 = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "Let's Jam!"}, @rsvp_approved_user)
# create RSVP request 2
@rsvp2 = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "Let's Jam!"}, @rsvp_declined_user)
# approve slot1
rs1 = RsvpRequestRsvpSlot.find_by_rsvp_slot_id(@slot1.id)
rs2 = RsvpRequestRsvpSlot.find_by_rsvp_slot_id(@slot2.id)
RsvpRequest.update({:id => rsvp.id, :session_id => @music_session.id, :rsvp_responses => [{:request_slot_id => rs1.id, :accept => true}, {:request_slot_id => rs2.id, :accept => false}]}, @session_creator)
rs1 = RsvpRequestRsvpSlot.find_by_rsvp_request_id_and_rsvp_slot_id(@rsvp1.id, @slot1.id)
rs2 = RsvpRequestRsvpSlot.find_by_rsvp_request_id_and_rsvp_slot_id(@rsvp1.id, @slot2.id)
RsvpRequest.update({:id => @rsvp1.id, :session_id => @music_session.id, :rsvp_responses => [{:request_slot_id => rs1.id, :accept => true}, {:request_slot_id => rs2.id, :accept => false}]}, @session_creator)
# reject slot1 and slot2
rs1 = RsvpRequestRsvpSlot.find_by_rsvp_request_id_and_rsvp_slot_id(@rsvp2.id, @slot1.id)
rs2 = RsvpRequestRsvpSlot.find_by_rsvp_request_id_and_rsvp_slot_id(@rsvp2.id, @slot2.id)
RsvpRequest.update({:id => @rsvp2.id, :session_id => @music_session.id, :rsvp_responses => [{:request_slot_id => rs1.id, :accept => false}, {:request_slot_id => rs2.id, :accept => false}]}, @session_creator)
end
def ensure_success(options = {})
@ -64,7 +82,11 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
find('div.creator-name', text: @session_creator.name)
# action button
find('#btn-action') if options[:show_cta]
if options[:show_cta]
find('#btn-action', :text => options[:button_text])
else
expect {find('#btn-action')}.to raise_error(Capybara::ElementNotFound)
end
# session details
find('div.scheduled_start')
@ -78,13 +100,13 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
# right sidebar - RSVPs
find('div.rsvp-details .avatar-tiny')
find('div.rsvp-details .rsvp-name', text: @rsvp_user.name)
find('div.rsvp-details .rsvp-name', text: @rsvp_approved_user.name)
find('div.rsvp-details img.instrument-icon')
# right sidebar - Still Needed
find('div.still-needed', text: @slot2.instrument.id.capitalize)
# right sidebar - Invited
# right sidebar - Pending Invitations
find('div[user-id="' + @session_invitee.id + '"]')
# comments
@ -92,7 +114,7 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
end
def ensure_failure
find('div.not-found', text: "SESSION NOT FOUND")
find('strong.not-found', text: "SESSION NOT FOUND")
end
describe "view" do
@ -103,43 +125,66 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
@music_session.save
# attempt to access with musician who was invited but didn't RSVP
sign_in_poltergeist(@session_invitee)
url = "/sessions/#{@music_session.id}/details"
visit url
ensure_success({:show_cta => true})
sign_in_poltergeist(@session_invitee)
visit @url
ensure_success({:show_cta => true, :button_text => 'RSVP NOW!'})
sign_out_poltergeist
# attempt to access with musician who wasn't invited
sign_in_poltergeist(@non_session_invitee)
visit @url
ensure_success({:show_cta => true, :button_text => 'RSVP NOW!'})
sign_out_poltergeist
# attempt to access with musician who RSVP'ed but wasn't approved
sign_in_poltergeist(@rsvp_declined_user)
visit @url
ensure_success({:show_cta => false})
sign_out_poltergeist
# attempt to access with musician who RSVP'ed and was approved
sign_in_poltergeist(@rsvp_approved_user)
visit @url
ensure_success({:show_cta => true, :button_text => 'CANCEL RSVP'})
sign_out_poltergeist
# attempt to access with session creator
sign_in_poltergeist(@session_creator)
visit @url
ensure_success({:show_cta => false})
sign_out_poltergeist
# attempt to access with musician who RSVP'ed but wasn't approved
# attempt to access with musician who RSVP'ed and was approved
end
it "should render only for session invitees for sessions with closed RSVPs before session starts" do
# attempt to access with musician who wasn't invited
# attempt to access with musician who was invited but didn't RSVP
sign_in_poltergeist(@session_invitee)
visit @url
ensure_success({:show_cta => true, :button_text => 'RSVP NOW!'})
sign_out_poltergeist
# attempt to access with musician who wasn't invited
sign_in_poltergeist(@non_session_invitee)
visit @url
ensure_failure # NON-INVITEE SHOULD NOT BE ABLE TO VIEW FOR CLOSED RSVPs
sign_out_poltergeist
# attempt to access with musician who RSVP'ed but wasn't approved
sign_in_poltergeist(@rsvp_declined_user)
visit @url
ensure_success({:show_cta => false})
sign_out_poltergeist
# attempt to access with musician who RSVP'ed and was approved
sign_in_poltergeist(@rsvp_approved_user)
visit @url
ensure_success({:show_cta => true, :button_text => 'CANCEL RSVP'})
sign_out_poltergeist
# attempt to access with session creator
sign_in_poltergeist(@session_creator)
visit @url
ensure_success({:show_cta => false})
sign_out_poltergeist
end
# musician_access = false, approval_required = false
@ -184,45 +229,13 @@ describe "Session Info", :js => true, :type => :feature, :capybara_feature => tr
# attempt to access with musician who RSVP'ed and was approved
end
it "should render all required information" do
# access with a user who was invited but hasn't RSVPed yet
# session creator
# date/time
# genre
# name
# description
# notation files
# language
# access
# legal info
# view comments
# sidebar - Approved RSVPs
# sidebar - Open Slots
# sidebar - Pending Invitations
end
it "should allow only RSVP approvals or session invitees to add comments" do
end
it "should show no call to action button if user has not RSVPed and all slots are taken" do
end
it "should show no call to action button if the session organizer is viewing" do
it "should show no call to action button for session organizer" do
end
end