* VRFS-1910 - approved_rsvps to handle null instruments on slots, as well as remove attr_accessors as holders on user model

This commit is contained in:
Seth Call 2014-07-10 15:22:00 -05:00
parent 3a5d146ef3
commit f693470c63
17 changed files with 221 additions and 68 deletions

View File

@ -475,40 +475,20 @@ module JamRuby
end
def access_description
return "#{musician_access_description}. #{fan_access_description}."
"#{musician_access_description}. #{fan_access_description}."
end
# retrieve users that have approved RSVPs
def approved_rsvps
users = User.find_by_sql(%Q{select u.id, u.photo_url, u.first_name, u.last_name, rs.instrument_id, ii.description, rs.proficiency_level, rr.id as rsvp_request_id, rrrs.id as rsvp_request_rsvp_slot_id
User.find_by_sql(%Q{select distinct ON(u.id) u.id, u.photo_url, u.first_name, u.last_name, json_agg(ii.id) as instrument_ids, json_agg(ii.description) as instrument_descriptions, json_agg(rs.proficiency_level) as instrument_proficiencies, json_agg(rr.id) as rsvp_request_ids
from rsvp_slots rs
inner join rsvp_requests_rsvp_slots rrrs on rrrs.rsvp_slot_id = rs.id
inner join rsvp_requests rr on rrrs.rsvp_request_id = rr.id
inner join instruments ii on ii.id = rs.instrument_id
left join instruments ii on ii.id = rs.instrument_id
inner join users u on u.id = rr.user_id
where rrrs.chosen = true and rs.music_session_id = '#{self.id}'
order by u.id}
where rrrs.chosen = true AND rs.music_session_id = '#{self.id}' AND rr.canceled != TRUE
group by u.id order by u.id}
)
users_collapsed_instruments = []
user = User.new
# build User array with instruments collapsed
users.each_with_index do |u, index|
if index == 0 || users[index].id != users[index-1].id
user = User.new
user.id = u.id
user.photo_url = u.photo_url
user.first_name = u.first_name
user.last_name = u.last_name
user.instrument_list = [{id: u.instrument_id, desc: u.description, level: u.proficiency_level}]
users_collapsed_instruments << user
else
user.instrument_list << {id: u.instrument_id, desc: u.description, level: u.proficiency_level}
end
end
users_collapsed_instruments
end
# get all slots for this session and perform a set difference with all chosen slots;

View File

@ -19,9 +19,6 @@ module JamRuby
# updating_password corresponds to a lost_password
attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json
# contains a list of instrument IDs (used to aggregate instruments under a user for various screens)
attr_accessor :instrument_list
# checks if user has submitted RSVP to a session
attr_accessor :has_rsvp

View File

@ -504,6 +504,22 @@ FactoryGirl.define do
factory :rsvp_request, class: JamRuby::RsvpRequest do
canceled false
cancel_all false
association :user, :factory => :user
# creates *number* slots for a new rsvp_request
factory :rsvp_request_for_multiple_slots do
ignore do
music_session nil
number nil
end
after(:create) { |rsvp_request, evaluator |
evaluator.number.times do |i|
slot = FactoryGirl.create(:rsvp_slot, music_session: evaluator.music_session, instrument: Instrument.order(:id).limit(1).offset(i).first, proficiency_level: 1)
FactoryGirl.create(:rsvp_request_rsvp_slot, chosen:true, rsvp_request: rsvp_request, rsvp_slot:slot)
end
}
end
end
factory :rsvp_request_rsvp_slot, class: JamRuby::RsvpRequestRsvpSlot do

View File

@ -209,6 +209,63 @@ describe MusicSession do
end
end
describe "approved_rsvps" do
it "aggregrates instrument info" do
creators_slot = music_session1.rsvp_slots[0]
music_session1.approved_rsvps.length.should == 1
approved_user = music_session1.approved_rsvps[0]
JSON.parse(approved_user[:instrument_ids])[0].should == creators_slot.instrument.id
JSON.parse(approved_user[:instrument_descriptions])[0].should == creators_slot.instrument.description
JSON.parse(approved_user[:instrument_proficiencies])[0].should == creators_slot.proficiency_level
JSON.parse(approved_user[:rsvp_request_ids])[0].should == creators_slot.rsvp_requests[0].id
end
it "unstructured rsvps should still be returned" do
music_session1.rsvp_slots.length.should == 1
creators_slot = music_session1.rsvp_slots[0]
# now take out the instrument and proficiency of the rsvp_slot (make it unstructured)
creators_slot.is_unstructured_rsvp = true
creators_slot.instrument = nil
creators_slot.proficiency_level = nil
creators_slot.save!
puts RsvpSlot.all.inspect
music_session = MusicSession.find(music_session1.id)
approved_rsvps = music_session.approved_rsvps
approved_rsvps.length.should == 1
approved_user = approved_rsvps[0]
JSON.parse(approved_user[:instrument_ids])[0].should == nil
JSON.parse(approved_user[:instrument_descriptions])[0].should == nil
JSON.parse(approved_user[:instrument_proficiencies])[0].should == nil
JSON.parse(approved_user[:rsvp_request_ids])[0].should == creators_slot.rsvp_requests[0].id
end
it "handles 2 instruments for a single request correctly" do
rsvp_request = FactoryGirl.create(:rsvp_request_for_multiple_slots, user: some_user, music_session: music_session1, number: 2)
approved_rsvps = music_session1.approved_rsvps
approved_rsvps.length.should == 2
# find the user who made the request for 2 rsvp slots in the approved_users array
approved_some_user = approved_rsvps.find {|s| puts s.inspect; s.id == some_user.id}
instrument_ids = JSON.parse(approved_some_user[:instrument_ids])
instrument_ids.should =~ rsvp_request.rsvp_slots.map {|slot| slot.instrument_id }
instrument_descriptions = JSON.parse(approved_some_user[:instrument_descriptions])
instrument_descriptions.should =~ rsvp_request.rsvp_slots.map {|slot| slot.instrument.description }
instrument_proficiencies = JSON.parse(approved_some_user[:instrument_proficiencies])
instrument_proficiencies.should =~ rsvp_request.rsvp_slots.map {|slot| slot.proficiency_level }
JSON.parse(approved_some_user[:rsvp_request_ids])[0].should == rsvp_request.id
end
end
describe "scheduled" do
it "excludes based on time-range" do
session = FactoryGirl.create(:music_session, scheduled_start: Time.now)

View File

@ -5,6 +5,7 @@
context.JK = context.JK || {};
context.JK.AccountSessionDetail = function (app) {
var EVENTS = context.JK.EVENTS;
var logger = context.JK.logger;
var rest = context.JK.Rest();
var sessionUtils = context.JK.SessionUtils;
@ -47,7 +48,13 @@
var rsvpCancelDlg = new context.JK.RsvpCancelDialog(app, sessionData.id, sessionData.rsvpId);
rsvpCancelDlg.initialize();
context.JK.app.layout.showDialog('rsvp-cancel-dialog');
app.layout.showDialog('rsvp-cancel-dialog')
.one(EVENTS.RSVP_CANCELED, function() {
refreshSessionDetail();
})
.one(EVENTS.DIALOG_CLOSED, function() {
$(this).unbind(EVENTS.RSVP_CANCELED);
});
}
@ -381,10 +388,6 @@
this.beforeShow = beforeShow;
this.afterShow = afterShow;
$(document).on("rsvpCancelEvent", function() {
refreshSessionDetail();
});
return this;
};

View File

@ -3,9 +3,10 @@
"use strict";
context.JK = context.JK || {};
context.JK.RsvpCancelDialog = function(app, sessionId, rsvpRequestId) {
var EVENTS = context.JK.EVENTS;
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $screen = null;
var $dialog = null;
var dialogId = 'rsvp-cancel-dialog';
var $btnCancel = $("#btnCancelRsvp");
@ -17,11 +18,11 @@
rest.getSessionHistory(sessionId)
.done(function(response) {
if (response) {
$('.session-name', $screen).html(response.name);
$('.scheduled-start', $screen).html(response.scheduled_start);
$('.session-name', $dialog).html(response.name);
$('.scheduled-start', $dialog).html(response.scheduled_start);
if (response.recurring_mode !== null) {
$('.schedule-recurrence', $screen).html("Recurs " + response.recurring_mode + " on this day at this time");
$('.schedule-recurrence', $dialog).html("Recurs " + response.recurring_mode + " on this day at this time");
}
}
})
@ -43,10 +44,10 @@
e.preventDefault();
var error = false;
var cancelOption = $('input[name="cancel"]:checked', $screen).val();
var cancelOption = $('input[name="cancel"]:checked', $dialog).val();
rest.cancelRsvpRequest(sessionId, rsvpRequestId, cancelOption)
.done(function(response) {
var comment = $.trim($('#txtComment', $screen).val());
var comment = $.trim($('#txtComment', $dialog).val());
if (comment.length > 0) {
rest.addSessionInfoComment(sessionId, comment)
.done(function(response) {
@ -54,20 +55,21 @@
})
.fail(function(xhr) {
error = true;
$('.error', $screen).html("Unexpected error occurred while saving message (" + xhr.status + ")");
$('.error', $screen).show();
$('.error', $dialog).html("Unexpected error occurred while saving message (" + xhr.status + ")");
$('.error', $dialog).show();
});
}
if (!error) {
$dialog.triggerHandler(EVENTS.RSVP_CANCELED);
app.layout.closeDialog(dialogId);
$btnCancel.trigger("rsvpCancelEvent");
}
})
.fail(function(xhr) {
$('.error', $screen).html("Unexpected error occurred while cancelling RSVP request (" + xhr.status + ")");
$('.error', $screen).show();
$('.error', $dialog).html("Unexpected error occurred while cancelling RSVP request (" + xhr.status + ")");
$('.error', $dialog).show();
});
return false;
});
}
@ -81,7 +83,7 @@
app.bindDialog(dialogId, dialogBindings);
$screen = $('[layout-id="' + dialogId + '"]');
$dialog = $('[layout-id="' + dialogId + '"]');
events();
}

View File

@ -98,6 +98,8 @@
$('.error', $screen).html(error.message);
$('.error', $screen).show();
});
return false;
});
}

View File

@ -97,7 +97,6 @@
function renderActiveSessions(sessions) {
$.each(sessions, function(i, session) {
logger.debug('Rendering session ID = ' + session.id);
sessionList.renderActiveSession(session, $(CATEGORY.ACTIVE.id));
});
@ -200,7 +199,6 @@
function renderScheduledSessions(sessions) {
$.each(sessions, function(i, session) {
logger.debug('Rendering session ID = ' + session.id);
sessionList.renderInactiveSession(session, $(CATEGORY.SCHEDULED.id));
});
afterLoadScheduledSessions(sessions);

View File

@ -30,7 +30,8 @@
context.JK.EVENTS = {
DIALOG_CLOSED : 'dialog_closed',
SHOW_SIGNUP : 'show_signup',
SHOW_SIGNIN : 'show_signin'
SHOW_SIGNIN : 'show_signin',
RSVP_CANCELED : 'rsvp_canceled'
}
context.JK.ALERT_NAMES = {

View File

@ -268,7 +268,7 @@
$.each(session.approved_rsvps, function(index, user) {
if (user.id == context.JK.currentUserId) {
$.each(user.instrument_list, function(index, instrument) {
instruments_me.push(instrument.desc);
instruments_me.push(instrument.desc ? instrument.desc : 'Any Instrument');
});
}
});

View File

@ -4,6 +4,7 @@
context.JK = context.JK || {};
context.JK.SessionList = function(app) {
var EVENTS = context.JK.EVENTS;
var gearUtils = context.JK.GearUtils;
var logger = context.JK.logger;
var rest = context.JK.Rest();
@ -130,8 +131,8 @@
var openSlots = false;
var hasInvitation = false;
var hasApprovedRsvp = false;
var currentUserHasRsvp = session.user_has_rsvp;
var approvedRsvpId = null; // if set, the user has an accepted RSVP
var pendingRsvpId = null; // if set, the user has a pending RSVP
var hasPendingOrDeclinedRsvp = false;
var openRsvps = session.open_rsvps;
@ -140,16 +141,22 @@
var i = 0;
var rsvpUsersHtml = '', openSlotsHtml = '', latencyHtml = '', notationFileHtml = '';
context._.each(session.pending_rsvp_requests, function(pending_rsvp_request) {
if(pending_rsvp_request.user_id == context.JK.currentUserId) {
pendingRsvpId = pending_rsvp_request.id;
}
});
// render users who have approved RSVPs
if (session.approved_rsvps && session.approved_rsvps.length > 0) {
for (i=0; i < session.approved_rsvps.length; i++) {
if (session.approved_rsvps[i].id === context.JK.currentUserId) {
hasApprovedRsvp = true;
context._.each(session.approved_rsvps, function(approved_rsvp) {
if (approved_rsvp.id === context.JK.currentUserId) {
approvedRsvpId = approved_rsvp.rsvp_request_id;
}
var rsvpUserInfo = createRsvpUser(session.approved_rsvps[i], session);
var rsvpUserInfo = createRsvpUser(approved_rsvp, session);
rsvpUsersHtml += rsvpUserInfo[0];
latencyHtml += rsvpUserInfo[1];
}
});
}
// this provides a buffer at the top to shift the first latency tag down in the event there are NO RSVP musicians
else {
@ -178,11 +185,48 @@
}
}
if ( (openRsvps || hasInvitation) && openSlots && !hasApprovedRsvp && !currentUserHasRsvp ) {
showRsvpLink = true;
}
else {
var showRsvpLink = true;
var noLinkText = '';
if (!openRsvps && !hasInvitation) {
showRsvpLink = false;
noLinkText = '<span class="text">You need an invitation to RSVP to this session.</span>';
}
else if(!openSlots) {
showRsvpLink = false
noLinkText = '<span class="text">No more openings in this session.</span>';
}
else if(approvedRsvpId) {
showRsvpLink = false;
noLinkText = $('<span class="text">You have been confirmed for this session. <a href="#">Cancel</a></span>');
noLinkText.find('a').click(function() {
var rsvpCancelDlg = new context.JK.RsvpCancelDialog(app, session.id, approvedRsvpId);
rsvpCancelDlg.initialize();
app.layout.showDialog('rsvp-cancel-dialog')
.one(EVENTS.RSVP_CANCELED, function() {
// VRFS-1891
})
.one(EVENTS.DIALOG_CLOSED, function() {
$(this).unbind(EVENTS.RSVP_CANCELED);
});
return false;
});
}
else if(pendingRsvpId) {
showRsvpLink = false;
noLinkText = $('<span class="text">You have RSVP\'ed to this session. <a href="#">Cancel</a></span>');
noLinkText.find('a').click(function() {
var rsvpCancelDlg = new context.JK.RsvpCancelDialog(app, session.id, pendingRsvpId);
rsvpCancelDlg.initialize();
app.layout.showDialog('rsvp-cancel-dialog')
.one(EVENTS.RSVP_CANCELED, function() {
// VRFS-1891
})
.one(EVENTS.DIALOG_CLOSED, function() {
$(this).unbind(EVENTS.RSVP_CANCELED);
});
return false;
});
}
// notation files
@ -215,10 +259,11 @@
var $parentRow = $('tr[data-session-id=' + session.id + ']', tbGroup);
$('.rsvp-link', $parentRow).click(function(evt) {
ui.launchRsvpSubmitDialog(session.id);
return false;
});
}
else {
$('.rsvp-msg', $parentRow).show();
$('.rsvp-msg', $parentRow).html(noLinkText).show();
}
}

View File

@ -1,3 +1,5 @@
@import "client/common";
div.findsession-scroll-container {
max-height:275px;
overflow-y:scroll;
@ -67,6 +69,15 @@ table.findsession-table, table.local-recordings {
a:hover {
color:#227985;
}
.rsvp-section {
text-align:center;
vertical-align:middle;
a {
color: $ColorLink;
}
}
}
.latency-grey {

View File

@ -9,4 +9,24 @@ module UsersHelper
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
image_tag(gravatar_url, alt: "#{user.first_name} #{user.last_name}", class: "#{hclass}")
end
# this must be a user created by the MusicSession.approved_rsvp method
def process_approved_rsvps(approved_rsvp)
instrument_list = []
# instruments are all denormalized into json arrays. fix em up into an instrument_list on the user object
instrument_ids = JSON.parse(approved_rsvp[:instrument_ids])
instrument_descs = JSON.parse(approved_rsvp[:instrument_descriptions])
instrument_proficiencies = JSON.parse(approved_rsvp[:instrument_proficiencies])
rsvp_request_id = JSON.parse(approved_rsvp[:rsvp_request_ids])[0] # there should always be only one
instrument_ids.each_with_index do |instrument_id, i|
desc = instrument_descs[i]
level = instrument_proficiencies[i]
instrument_list.push({ id: instrument_id, desc: desc, level: level})
end
instrument_list
end
end

View File

@ -31,10 +31,6 @@ else
[item.genre.description] # XXX: need to return single genre; not array
end
node :user_has_rsvp do |history|
current_user.has_rsvp(history)
end
node :scheduled_start do |history|
history.scheduled_start.utc.strftime("%a %e %B %Y %H:%M:%S") if history.scheduled_start
end
@ -91,10 +87,14 @@ else
}
child({:approved_rsvps => :approved_rsvps}) {
attributes :id, :photo_url, :first_name, :last_name, :instrument_list, :name, :resolved_photo_url
attributes :id, :photo_url, :first_name, :last_name, :name, :resolved_photo_url, :rsvp_request_id
node do |user|
{ latency: user_score(user.id), name: user.name }
{
latency: user_score(user.id),
instrument_list: process_approved_rsvps(user),
rsvp_request_id: JSON.parse(user.rsvp_request_ids)[0] # there must always be a rsvp_request_id; and they should all be the same
}
end
}

View File

@ -203,7 +203,7 @@
<tr><td><span class="bold">Legal Policy:</span><br/>{legal_policy}</td></tr>
</table>
</td>
<td class="noborder" style="text-align:center; vertical-align:middle;">
<td class="noborder rsvp-section">
<span class="rsvp-msg" style="display:none;">You cannot RSVP to this session.</span>
<a class="rsvp-link" style="display:{rsvp_link_display_style};">
<%= image_tag "content/icon_join.png", :size => "19x22" %>

View File

@ -177,5 +177,21 @@ describe "Find Session", :js => true, :type => :feature, :capybara_feature => tr
# TODO: verify that the UI works - after VRFS-1892
end
it "RSVP text shows correctly" do
music_session = FactoryGirl.create(:music_session, creator: user, is_unstructured_rsvp: true)
fast_signin(user, Nav.find_session)
find("#sessions-scheduled .rsvp-msg span.text", text: "You have been confirmed for this session. ")
sign_out
go_to_root
fast_signin(finder, Nav.find_session)
find("#sessions-scheduled .rsvp-msg span.text", text: "You have been confirmed for this session. ")
end
end
end

View File

@ -163,6 +163,11 @@ def open_user_dropdown
end
def go_to_root
visit '/'
should_be_at_root
end
def should_be_at_root
find('h1', text: 'Play music together over the Internet as if in the same room')
end