VRFS-2311 allow session creator to add and approve slots on the fly when there are no available slots

This commit is contained in:
Brian Smith 2014-10-25 22:23:52 -04:00
parent bcfc2def9e
commit c57ea372ac
18 changed files with 309 additions and 60 deletions

View File

@ -27,6 +27,7 @@ require "jam_ruby/constants/validation_messages"
require "jam_ruby/errors/permission_error"
require "jam_ruby/errors/state_error"
require "jam_ruby/errors/jam_argument_error"
require "jam_ruby/errors/conflict_error"
require "jam_ruby/lib/app_config"
require "jam_ruby/lib/s3_manager_mixin"
require "jam_ruby/lib/module_overrides"

View File

@ -0,0 +1,5 @@
module JamRuby
class ConflictError < Exception
end
end

View File

@ -724,7 +724,7 @@ module JamRuby
query = query.offset(offset)
query = query.limit(limit)
query = query.where("music_sessions.create_type IS NULL OR music_sessions.create_type != ?", MusicSession::CREATE_TYPE_QUICK_START)
query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE)
query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank?
query = query.where('music_sessions.language = ?', lang) unless lang.blank?
query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank?

View File

@ -154,7 +154,7 @@ module JamRuby
# authorize the user attempting to respond to the RSVP request
if music_session.creator.id != user.id
raise PermissionError, "Only the session organizer can accept or decline and RSVP request."
raise PermissionError, "Only the session organizer can accept or decline an RSVP request."
end
rsvp_request = RsvpRequest.find_by_id(rsvp_request_id)
@ -181,8 +181,35 @@ module JamRuby
raise StateError, "Slot does not exist"
end
# slot has already been accepted
if rsvp_slot.chosen && r[:accept]
raise StateError, "All RSVP slots for the #{rsvp_slot.instrument_id} have been already approved."
# get the instrument and skill level for this slot and see if there are others available
# and auto-reassign the request_slot to the open rsvp_slot
open_slots = music_session.open_slots
# don't worry about matching proficiency if the user RSVPed to "Any Skill Level"
if rsvp_slot.proficiency_level == 0
open_slots = open_slots.select { |slot| slot.instrument_id == rsvp_slot.instrument_id }
else
open_slots = open_slots.select { |slot| slot.instrument_id == rsvp_slot.instrument_id && slot.proficiency_level == rsvp_slot.proficiency_level }
end
# (1) another available slot exists matching this instrument and proficiency
unless open_slots.blank?
rsvp_slot = open_slots.first
request_slot.rsvp_slot_id = rsvp_slot.id # this links the RsvpRequestRsvpSlot to the available open slot matching this instrument and proficiency level
# (2) no identical slots available => create a new one on the fly
else
new_slot = RsvpSlot.new
new_slot.instrument_id = rsvp_slot.instrument_id
new_slot.proficiency_level = rsvp_slot.proficiency_level
new_slot.music_session_id = rsvp_slot.music_session_id
new_slot.is_unstructured_rsvp = rsvp_slot.is_unstructured_rsvp
new_slot.save!
request_slot.rsvp_slot_id = new_slot.id # saved below on line 218
end
end
if r[:accept]

View File

@ -228,7 +228,47 @@ describe RsvpRequest do
n.description.should == NotificationTypes::SCHEDULED_SESSION_RSVP_APPROVED
end
it "should not allow approval of RSVP for a slot that has already been approved" do
it "should allow approval of multiple RSVPs to same slot ID when multiple slots exist for same instrument and proficiency level" do
@music_session.open_rsvps = true
@music_session.save!
@slot2.delete
slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('electric guitar'))
slot2.save!
rsvp1 = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id], :message => "Let's Jam!"}, @session_invitee)
rsvp2 = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id], :message => "Let's Jam!"}, @non_session_invitee)
rs1 = RsvpRequestRsvpSlot.find_by_rsvp_request_id(rsvp1.id)
rs1.rsvp_slot_id.should == @slot1.id
# approve RSVP 1
RsvpRequest.update({:id => rsvp1.id, :session_id => @music_session.id, :rsvp_responses => [{:request_slot_id => rs1.id, :accept => true}]}, @session_creator)
# ensure slot ID didn't change
rs1 = RsvpRequestRsvpSlot.find_by_rsvp_request_id(rsvp1.id)
rs1.rsvp_slot_id.should == @slot1.id
# check before approving
rs2 = RsvpRequestRsvpSlot.find_by_rsvp_request_id(rsvp2.id)
rs2.rsvp_slot_id.should == @slot1.id
# approve RSVP 2 (same slot ID, but slot2 is same instrument + proficiency level)
RsvpRequest.update({:id => rsvp2.id, :session_id => @music_session.id, :rsvp_responses => [{:request_slot_id => rs2.id, :accept => true}]}, @session_creator)
# should have linked the RsvpRequestRsvpSlot record to the second slot
rs2 = RsvpRequestRsvpSlot.find_by_rsvp_request_id(rsvp2.id)
rs2.rsvp_slot_id.should == slot2.id
slots = RsvpSlot.where("music_session_id = ?", @music_session.id)
slots.count.should == 2
request_slots = RsvpRequestRsvpSlot.where("chosen=true")
request_slots.count.should == 2
end
it "should create a new slot for an RSVP for a slot that has already been approved" do
rsvp = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "Let's Jam!"}, @session_invitee)
# approve
@ -239,7 +279,10 @@ describe RsvpRequest do
# approve again
rs1 = RsvpRequestRsvpSlot.find_by_rsvp_slot_id(@slot1.id)
rs2 = RsvpRequestRsvpSlot.find_by_rsvp_slot_id(@slot2.id)
expect {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 => true}]}, @session_creator)}.to raise_error(StateError)
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 => true}]}, @session_creator)
slots = RsvpSlot.where("music_session_id = ?", @music_session.id)
slots.count.should == 4
end
end

View File

@ -31,7 +31,6 @@
function beforeShow(data) {
sessionId = data.id;
console.log("sessionId=%o", sessionId);
loadSessionData();
}
@ -41,8 +40,7 @@
function inviteMusicians(e) {
e.preventDefault();
friendInput = inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians',
sessionId);
friendInput = inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', sessionId);
inviteMusiciansUtil.loadFriends();
$(friendInput).show();
// invitationDialog.showEmailDialog();
@ -85,12 +83,55 @@
e.preventDefault();
var rsvpId = $(e.target).attr('request-id');
var userName = $(e.target).attr('user-name');
var instrumentIds = $(e.target).attr('data-instrument-text');
var params = buildRsvpRequestActionParams(rsvpId, true);
// first check if any open slots exist for these instruments
rest.getOpenSessionSlots(sessionData.id, true)
.done(function(openSlots) {
if (openSlots) {
if (openSlots.length === 0) {
ui.launchRsvpCreateSlotDialog(sessionData.id, instrumentIds.split('|'), userName);
}
else {
var arrInstrumentIds = instrumentIds.split('|');
var openSlotInstrumentIds = [];
var unavailableSlotInstrumentIds = [];
// ensure each instrument in the user's list is available in the open slots list
$.each(openSlots, function(index, slot) {
openSlotInstrumentIds.push(slot.instrument_id);
});
// build list of instrument IDs in the RSVP request for which there are no open slots
for (var i=0; i < arrInstrumentIds.length; i++) {
if ($.inArray(arrInstrumentIds[i], openSlotInstrumentIds) === -1) {
unavailableSlotInstrumentIds.push(arrInstrumentIds[i]);
}
}
if (unavailableSlotInstrumentIds.length > 0) {
ui.launchRsvpCreateSlotDialog(sessionData.id, unavailableSlotInstrumentIds, userName, function() {
approve(rsvpId, params);
});
}
else {
approve(rsvpId, params);
}
}
}
else {
ui.launchRsvpCreateSlotDialog(sessionData.id, instrumentIds.split('|'), userName);
}
});
}
function approve(rsvpId, params) {
rest.updateRsvpRequest(rsvpId, params)
.done(refreshSessionDetail)
.fail(function(jqXHR, textStatus, errorMessage) {
if (jqXHR.status === 400) {
if (jqXHR.status === 409) {
app.notify(
{
title: "Unable to Approve RSVP",
@ -284,11 +325,14 @@
var latencyHtml = "";
$.each(sessionData.pending_rsvp_requests, function(index, pending_rsvp_request) {
if (pending_rsvp_request.user_id != context.JK.currentUserId) {
var instrumentDesc = [];
if ("instrument_list" in pending_rsvp_request && pending_rsvp_request.instrument_list != null) {
$.each(pending_rsvp_request.instrument_list, function (index, instrument) {
var instrumentId = instrument == null ? null : instrument.id;
var inst = context.JK.getInstrumentIcon24(instrumentId);
instrumentLogoHtml += '<img title="' + instrumentId + '" hoveraction="instrument" data-instrument-id="' + instrumentId + '" src="' + inst + '" width="24" height="24" />&nbsp;';
instrumentDesc.push(instrumentId);
})
}
@ -304,7 +348,7 @@
$templateAccountPendingRsvp.html(),
{user_id: pending_rsvp_request.user_id, avatar_url: avatar_url,
user_name: pending_rsvp_request.user.name, instruments: instrumentLogoHtml,
latency: latencyHtml, request_id: pending_rsvp_request.id},
latency: latencyHtml, request_id: pending_rsvp_request.id, instrument_text: instrumentDesc.join('|')},
{variable: 'data'}
);

View File

@ -35,7 +35,7 @@
}
function showDialog() {
return app.layout.showDialog('rsvp-cancel-dialog');
return app.layout.showDialog(dialogId);
}
function events() {
@ -99,7 +99,7 @@
$dialog = $('[layout-id="' + dialogId + '"]');
$("#rsvp-cancel-dialog").iCheck({
$dialog.iCheck({
checkboxClass: 'icheckbox_minimal',
radioClass: 'iradio_minimal',
inheritClass: true

View File

@ -0,0 +1,64 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.RsvpCreateSlotDialog = function(app, sessionId, instrumentIds, rsvpRequesterName, createSlotsCallback) {
var EVENTS = context.JK.EVENTS;
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $dialog = null;
var dialogId = 'rsvp-create-slot-dialog';
var $btnSave = null;
function beforeShow(data) {
}
function afterShow(data) {
var instructions = "All RSVP slots for the instruments below have already been filled. Would you like to open up a new RSVP slot for " + rsvpRequesterName + " so that they can join your session too?";
var instruments = instrumentIds.join("<br/>");
$('.instructions', $dialog).html(instructions);
$('.instruments', $dialog).html(instruments);
}
function afterHide() {
}
function showDialog() {
return app.layout.showDialog(dialogId);
}
function events() {
$btnSave.unbind('click');
$btnSave.click(function(e) {
e.preventDefault();
if (createSlotsCallback) {
createSlotsCallback();
app.layout.closeDialog(dialogId);
}
});
}
function initialize() {
var dialogBindings = {
'beforeShow' : beforeShow,
'afterShow' : afterShow,
'afterHide': afterHide
};
app.bindDialog(dialogId, dialogBindings);
$dialog = $('[layout-id="' + dialogId + '"]');
$btnSave = $dialog.find('.btnSave');
events();
}
this.initialize = initialize;
this.showDialog = showDialog;
}
return this;
})(window,jQuery);

View File

@ -30,17 +30,14 @@
var hasOpenSlots = response.open_slots && response.open_slots.length > 0;
if (response['is_unstructured_rsvp?']) {
var checkedState = hasOpenSlots ? '' : 'checked="checked"'
$('.rsvp-instruments', $dialog).append('<input type="checkbox" ' + checkedState + ' value="unstructured"/>Play Any Instrument You Like<br/>');
$('.rsvp-instruments', $dialog).append('<input type="checkbox" ' + checkedState + ' value="unstructured"/><span>Play Any Instrument You Like</span><br/>');
}
if (hasOpenSlots) {
$.each(response.open_slots, function(index, val) {
var instrument = val.instrument_id;
$('.rsvp-instruments', $dialog).append('<input type="checkbox" value="' + val.id + '"/>' + val.description + " (" + val.proficiency_desc + ")<br/>");
$('.rsvp-instruments', $dialog).append('<input type="checkbox" data-instrument-id="' + val.instrument_id + '" value="' + val.id + '"/><span>' + val.description + " (" + val.proficiency_desc + ")</span><br/>");
});
}
}
@ -60,18 +57,28 @@
$btnSubmit.click(function(e) {
e.preventDefault();
var error = false;
var slotIds = [];
var selectedSlots = [];
$("input:checked", '.rsvp-instruments').each(function(index) {
var selection = $(this).attr('data-instrument-id');
if ($.inArray(selection, selectedSlots) > -1) {
$('.error', $dialog).html('You have selected the same instrument twice.').show();
error = true;
return;
}
selectedSlots.push(selection);
slotIds.push($(this).val());
});
if (error) return;
if (slotIds.length === 0) {
$('.error', $dialog).show();
$('.error', $dialog).html('You must select at least 1 instrument.').show();
return;
}
var error = false;
// TODO: show spinner??
rest.submitRsvpRequest(sessionId, slotIds)
.done(function(response) {
@ -83,8 +90,7 @@
})
.fail(function(xhr, textStatus, errorMessage) {
error = true;
$('.error', $dialog).html("Unexpected error occurred while saving message (" + xhr.status + ")");
$('.error', $dialog).show();
$('.error', $dialog).html("Unexpected error occurred while saving message (" + xhr.status + ")").show();
});
}

View File

@ -422,7 +422,7 @@
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('SESSION DETAILS');
$action_btn.click(function() {
context.JK.popExternalLink('/sessions/' + payload.session_id + '/details');
openSessionInfoWebPage({"session_id": payload.session_id});
});
}
@ -843,10 +843,12 @@
}, [{
id: "btn-more-info",
text: "MORE INFO",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -865,10 +867,12 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-manage-rsvp",
text: "Manage RSVP",
"layout-action": "close",
href: "/client#/account/sessionDetail/" + payload.session_id,
"class": "button-orange"
text: "MANAGE RSVP",
"class": "button-orange",
callback: navigateToSessionDetails,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -886,11 +890,14 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-session-details",
text: "Session Details",
text: "SESSION DETAILS",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -908,11 +915,13 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-session-details",
text: "Session Details",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
text: "SESSION DETAILS",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -930,11 +939,13 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-session-details",
text: "Session Details",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
text: "SESSION DETAILS",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -952,11 +963,13 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-session-details",
text: "Session Details",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
text: "SESSION DETAILS",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -974,11 +987,13 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-session-details",
text: "Session Details",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
text: "SESSION DETAILS",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -996,11 +1011,13 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-session-details",
text: "Session Details",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
text: "SESSION DETAILS",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -1019,11 +1036,13 @@
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, [{
id: "btn-session-details",
text: "Session Details",
"layout-action": "close",
href: JK.root_url + "/sessions/" + payload.session_id + "/details",
text: "SESSION DETAILS",
rel: "external",
"class": "button-orange"
"class": "button-orange",
callback: openSessionInfoWebPage,
callback_args: {
"session_id": payload.session_id
}
}]
);
});
@ -1231,6 +1250,14 @@
context.JK.popExternalLink('/recordings/' + args.recording_id);
}
function navigateToSessionDetails(args) {
context.location = '/client#/account/sessionDetail/' + args.session_id;
}
function openSessionInfoWebPage(args) {
context.JK.popExternalLink('/sessions/' + args.session_id + '/details');
}
function deleteNotificationHandler(evt) {
evt.stopPropagation();
var notificationId = $(this).attr('notification-id');

View File

@ -47,12 +47,19 @@
return rsvpDialog.showDialog();
}
function launchRsvpCreateSlotDialog(sessionId, instrumentIds, rsvpRequesterName, createSlotsCallback) {
var rsvpDialog = new JK.RsvpCreateSlotDialog(JK.app, sessionId, instrumentIds, rsvpRequesterName, createSlotsCallback);
rsvpDialog.initialize();
return rsvpDialog.showDialog();
}
this.addSessionLike = addSessionLike;
this.addRecordingLike = addRecordingLike;
this.launchCommentDialog = launchCommentDialog;
this.launchShareDialog = launchShareDialog;
this.launchRsvpSubmitDialog = launchRsvpSubmitDialog;
this.launchRsvpCancelDialog = launchRsvpCancelDialog;
this.launchRsvpCreateSlotDialog = launchRsvpCreateSlotDialog;
return this;
};

View File

@ -88,7 +88,7 @@
function togglePlay() {
if(playing) {
$status.text('SESSION IN PROGRESS');
$status.text('LIVE SESSION IN PROGRESS');
stopPlay();
}
else {

View File

@ -16,6 +16,10 @@ class ApiController < ApplicationController
@exception = exception
render "errors/permission_error", :status => 403
end
rescue_from 'JamRuby::ConflictError' do |exception|
@exception = exception
render "errors/conflict_error", :status => 409
end
rescue_from 'ActiveRecord::RecordNotFound' do |exception|
@@log.debug(exception)
render :json => { :errors => { :resource => ["record not found"] } }, :status => 404

View File

@ -95,7 +95,7 @@
{{data.latency}}
%td.rsvp-buttons
%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 approveRsvpRequest', 'data-instrument-text' => "{{data.instrument_text}}", 'user-id' => "{{data.user_id}}", 'request-id' => "{{data.request_id}}", 'user-name' => "{{data.user_name}}"} APPROVE
%a{href: "#", class: 'button-orange left declineRsvpRequest', 'user-id' => "{{data.user_id}}", 'request-id' => "{{data.request_id}}"} DECLINE
.clearall
.clearall

View File

@ -17,6 +17,7 @@
= render 'dialogs/commentDialog'
= render 'dialogs/rsvpSubmitDialog'
= render 'dialogs/rsvpCancelDialog'
= render 'dialogs/rsvpCreateSlotDialog'
= render 'dialogs/sessionCancelDialog'
= render 'dialogs/signinDialog'
= render 'dialogs/signupDialog'

View File

@ -0,0 +1,13 @@
.dialog.dialog-overlay-sm layout='dialog' layout-id='rsvp-create-slot-dialog' id='rsvp-create-slot-dialog'
.content-head
= image_tag "content/icon_alert.png", {:width => 24, :height => 24, :class => 'content-icon' }
h1 RSVP Slot Already Filled
.dialog-inner
div.instructions
br
div.instruments
.buttons
.right
a.button-grey class='btnCancel' layout-action='cancel' NO
a.button-orange class='btnSave' YES

View File

@ -9,7 +9,7 @@
.schedule-recurrence
.part
.slot-instructions Check the box(es) next to the track(s) you want to play in the session:
.error{:style => 'display:none'} You must select at least 1 instrument.
.error{:style => 'display:none'}
.rsvp-instruments
.comment-instructions Enter a message to the other musicians in the session (optional):

View File

@ -0,0 +1,7 @@
object @exception
attributes :message
node "type" do
"ConflictError"
end